|
62718
|
2223
|
6
|
2026-05-20T12:01:12.923775+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278472923_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
7 files, folder partially checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
4 modified...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"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":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"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":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"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":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"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":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"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":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"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":"AXStaticText","text":"7 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.15083799,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.16839585,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08111702,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.2386273,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":7,"bounds":{"left":0.2835771,"top":0.25618514,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit 2 files, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptServiceTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.09009308,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Kiosk/AutomatedReports 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.34397447,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsServiceTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.3790902,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.16839585,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08111702,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.2386273,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":6,"bounds":{"left":0.2835771,"top":0.25618514,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08111702,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08111702,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.2386273,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":5,"bounds":{"left":0.2835771,"top":0.25618514,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":4,"bounds":{"left":0.2835771,"top":0.25618514,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.273743,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit 2 files, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptServiceTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.09009308,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Kiosk/AutomatedReports 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.34397447,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsServiceTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptServiceTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.09009308,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptServiceTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.09009308,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Kiosk/AutomatedReports 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.34397447,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsServiceTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsServiceTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.3790902,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unversioned Files 9 files not checked","depth":4,"bounds":{"left":0.26462767,"top":0.39664805,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"}]...
|
4946939455601764587
|
8862143054946383452
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
7 files, folder partially checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
4 modified...
|
62716
|
NULL
|
NULL
|
NULL
|
|
62715
|
2222
|
5
|
2026-05-20T12:01:11.435787+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278471435_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
7 files, folder partially checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit 2 files, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptServiceTest.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Kiosk/AutomatedReports 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsServiceTest.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":5,"on_screen":true,"role_description":"text"}]...
|
3265721890145454603
|
8860102361382007900
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
7 files, folder partially checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62716
|
2223
|
5
|
2026-05-20T12:01:11.435775+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278471435_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
7 files, folder partially checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Component/AskAnything 1 file, folder checked...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"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":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"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":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"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":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"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":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"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":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"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":"AXStaticText","text":"7 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.15083799,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.16839585,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08111702,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.2386273,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":7,"bounds":{"left":0.2835771,"top":0.25618514,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit 2 files, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptServiceTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.09009308,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Kiosk/AutomatedReports 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.34397447,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsServiceTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.3790902,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.16839585,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08111702,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.2386273,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":6,"bounds":{"left":0.2835771,"top":0.25618514,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/AskAnything 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"}]...
|
-4349701781950362500
|
8860102361382007900
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
7 files, folder partially checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptServiceTest.php, class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Component/AskAnything 1 file, folder checked
AskAnythingPromptService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Component/AskAnything 1 file, folder checked...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62714
|
2223
|
4
|
2026-05-20T12:01:08.539040+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278468539_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2800545296294351714
|
235049485056389185
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
PhostormVIewINavigarecodeLaravelKeractorTOOISWindowmelpFV faVsco.js#12098 on JY-20676-delete-report-related-olProleteyYC) ActivitvController.pnpphp api.php© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php xAsKAnytIngkepository.ong© AutomatedReportsServiceTest.phpD Events© AskAnythingPromptDto.php© AsKAnytningPromptservice.ongc Historyservice.ong© AskJiminnyReportsController.php© AutomatedReportsService.php©) AskAnythingPromptServiceTest.phpD AskJiminnyAiclass ASKAnyth1ngPromptServiceWAWSpublic function __construct(privace readonly broupkepostcoryincertace sgroupkepostcory.u cache2{...}w countryD CustomerAp* drecucn arrau<askanuchinorrompcurosDatabase→ Datadogpublic function getUser Suser. AskAnvthingPromptlarget Starget: arravDatettimeDealinsiahtsspromots = Sthis->askanvthznaRepos1torv->f1ndPrompts8vUserAndtargetN DealRisks1N GlasticSearchStargetM EloquentEncodingLocal ChangesShelfConsoleChanges 7 files→ =Do not ignoreE.env.local app0202a0550apo/Hito/controllers/AP|IActivitvController.ono© ActivityController.php app/Http/Controllers/API© AskAnythingPromptService.php app/Component/AskAnything© AskAnythingPromptServiceTest.php tests/Unit/Component/AskAnything© AutomatedReportsServiceTest.php tests/Unit/Services/Kiosk/Automated->aetManagerol->setSerializernew JsonSerjalizeron:© JiminnyDebuqCommand.php app/Console/Commandsphp loaaina.php confiareturn Sthis->resnonse->withltemdv Unversioned Files 9 filesSsearch.= .env.nikilocal appE.env.other appSsearchTransformen->withConsumer (Suser)c) canAccessAlRevoris lest.ono tests/Unit/Policies© CreateMockAskJiminnvReportResultCommand.php app/Console/Commands/Rek tavicon.ico oublidEids.txt apd1eraw sal querv sal apo© SimulateWebhooksCommand.php app/Console/Commands/Crm/HubspotM. WEBHOOK FILTERING IMPLEMENTATION.md aoo© Search.phpA2M2^supoont Dally • now100% S2• Wed 20 May 15:01:08AutomatedReportsServiceTest ~=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyCascadeA console [STAGING]C) CoachinaFeedbackCoachUserin.onpv 208212218220Testing SmsReceivedFix UserinvitationdteReviewing ASKAnything+0 ..Tx: AutovPlavaroundvSo jiminny v021 A1 A18 V.2 Y6 ^atorromptservace-plpaelos . wilSELECT * FROM automated reports where id = 67:SELECT * FROM automated_ reports where 10 = 421You're right to question this. Let me clarify:SELECT * FROM Users WHERE id = 143: # group 281. The hasReports Parameterselect * from teams where 1d = 31457I actually identified this as required in my review — it was listed as "Still Open" issue #4. The edit() method was missingthe 8th parameter, causina the frontend to receive no has reports field after an edit. which breaks the delete quard forselect * from crm confiqurations where 1d = 500:promots that nave reportsselect * from users where name = 'Integration Account"; # 1695Without it: After editina a oromot that backs a report. has reports is missina → frontend talls back to false → deleteSELE * FROM sochal accounts WHERE soctable 10 = 16951View allnatedReports/AutomatedReportsServiceTest.pho ÷28-1select * from activities where crm confiquration id = 39aon/Comoonent/AskAnvthina/MAskAnvthinaPromotService.onvand recording_state = 'recorded' and duration > 60* Reiect allAccept alland status =12025-12-011Ask anything (&+L)SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•C° Adantivew1 difterencecurrent versionollers APl > Activityec->getManager()->setSerializer(new JsonSerializer):Ssearch->loadCount('automatedRenorts'):returnSthis->resnonse->withTtemdSsearch.ScpanchTnansfanmen-swithCancumen (Sucen)WN Windsurf Toams 27-8 UTF.8|Po 4 spaces...
|
62712
|
NULL
|
NULL
|
NULL
|
|
62713
|
2222
|
4
|
2026-05-20T12:01:07.186779+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278467186_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2Shell|Edit ViewSessionScripts|ProfilesWind iTerm2Shell|Edit ViewSessionScripts|ProfilesWindowHelpAPP (-zsh)DOCKERO 81DEV (-zsh)₴2APP (-zsh)Updating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.phpapp/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php273103714731402112+*app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.phpapp/Services/RecallAI/RecallAIService.php5311+++++++++front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests._/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DTOs/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallA1/RecallAlServiceTest.php60552416150++++++[PHONE] files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminskiandcontributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5690/5690 C8100%Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ ||Support Daily • nowA100% С8• Wed 20 May 15:01:06Support Dailynow - 15:00-15:15= Notes - Support Daily.C Join Google MeetAPP...
|
NULL
|
-1688877752538392712
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2Shell|Edit ViewSessionScripts|ProfilesWind iTerm2Shell|Edit ViewSessionScripts|ProfilesWindowHelpAPP (-zsh)DOCKERO 81DEV (-zsh)₴2APP (-zsh)Updating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.phpapp/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php273103714731402112+*app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.phpapp/Services/RecallAI/RecallAIService.php5311+++++++++front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests._/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DTOs/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallA1/RecallAlServiceTest.php60552416150++++++[PHONE] files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminskiandcontributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5690/5690 C8100%Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ ||Support Daily • nowA100% С8• Wed 20 May 15:01:06Support Dailynow - 15:00-15:15= Notes - Support Daily.C Join Google MeetAPP...
|
62711
|
NULL
|
NULL
|
NULL
|
|
62712
|
2223
|
3
|
2026-05-20T12:01:04.592941+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278464592_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigareCodeLaravelKeractorTOOISWindo PhostormVIewINavigareCodeLaravelKeractorTOOISWindowmelpFV faVsco.js#12098 on JY-20676-delete-report-related-objProleteyActivityController.php© AskAnythingPromptService.phpxphp api.php© AskAnythingPromptDto.phpD Events©AsKAnytningPromptservice.ongAsKAnytIngkepository.ongAskAnythingController.php© AutomatedReportsServiceTest.php© AskAnythingPromptDto.phpc Historyservice.ongD AskJiminnyAiWAWSBillingManagementu cache© AskJiminnyReportsController.php©AutomatedReportsService.php©) AskAnythingPromptServiceTest.phpclass AskAnychzngPromptserv1cepublic function _construct(private readonly broupreposttoryintertace sgroupkepository,) 4.3w countryD CustomerAp* drecucn arrau<askanuchinorrompcurosDatabaseDatadogDatettimeDeallnsightsN DealRisks• ElasticSearch• EloquentD Encodingpublic function getUser Suser. AskAnvthingPromptlarget Starget: arravspromots = Sthis->askanvthznaRepos1torv->f1ndPrompts8vUserAndtargetStargetLocal ChangesShelfConsoleChanges 7 files= .env.local app© ActivityController.php app/Http/Controllers/API© AskAnythingPromptService.phpapp/Component/AskAnything© AskAnythingPromptServiceTest.php tests/Unit/Component/AskAnything© AutomatedReportsServiceTest.php tests/Unit/Services/Kiosk/AutomatedReportTJP + → Side-by-side viewer -Do not ignoren202a0550apo/console/Commands/JiminnvDebuaCommand.ononamespace Jiminny Console Commands:use Illuminate\Console\Command;php loaaina.php confiause Jiminny Models\Team;Unversioned Files 9 files=.env.nikilocal appE.env.other app/*** Class JiminnyDebugCommandc) canAccessAlRevoris lest.ono tests/Unit/Policies©CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Re* @package Jiminny\Console\Commandsk tavicon.ico oublidE ids.txt appclass JiminnyDebugCommand extends Command1eraw sal querv sal apo© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspotprotected $signature = 'jiminny:debug';M. WEBHOOK FILTERING IMPLEMENTATION.md aoolpublic function handle(): voidSteam = Team: :find0rFail(2);SusersIds = $team->users() -›pluck('users.id') ->toArray();var dumo Susersids)*dnelexit(1);$0supoont Dally • nowU AutomatedReportsServiceTestv100% L28• Wed 20 May 15:01:04E custom.logA console [STAGING]E laravel.log4 SF jiminny@localhost] xC) CoachinaFeedbackCoachUserin.onpTx: AutovPlavaroundA HS_Jocal [jiminny@localhost]A console [PROD]# console [euyCascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 •fajiminny© Search.phpA2X2A~ 208021 A1 A18 V.2 Y6 ^atorromptservace-plpaelos . wilSELECT * FROM automated_reports where id = 67;SELECT * FROM automated reports where 10 = 421SELECT * FROM users WHERE id = 143; # group 28You're right to question this. Let me clarify:212select * from teams where id = 3143;select * from crm confiqurations where 1d = 500:select * from users where name = 'Integration Account'; # 1695SELE * FROM sochal accounts WHERE soctable 10 = 169511. The hasReports ParameterI actually identified this as required in my review — it was listed as "Still Open" issue #4. The edit() method was missingthe 8th parameter, causing the frontend to receive no has_reports field after an edit, which breaks the delete guard forpromots that nave reportsWithout it: After editing a prompt that backs a report, has_reports is missing frontend falls back to false → deleteView allmatedReports/# AutomatedReportsServiceTest.oho ÷28-1aon/Comoonent/AskAnvthina/MAskAnvthinaPromotService.onp218219220— 221select * from activities where erm confiquration id = 39and recording_state = 'recorded' and duration › 60and status = 'comoleted' and actual start time >=12025-12-011© Reject allAccept allSSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•S AdaptiveX ta B ?1differencescurrent versionnamespace Jiminny Console Commands:use Illuminate\Console\Command;use Illuminate\Support\Facades\Redis;use Jiminny\Models\Team;* Class LiminnyDebugCommand* @package Jiminny\Console\Commandsclass JiminnyDebugCommand extends Commandprotected Ssignature = 'jiminny:debug (action?}';public function handle(): void$action = $this->argument('action');if (Saction ==='redis-set') {Sthis->testRed1sSeto:return:exit(1):WN Windsurf Toams 27-8 UTF.8.f?4 spaces...
|
NULL
|
-7212889765303651029
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavigareCodeLaravelKeractorTOOISWindo PhostormVIewINavigareCodeLaravelKeractorTOOISWindowmelpFV faVsco.js#12098 on JY-20676-delete-report-related-objProleteyActivityController.php© AskAnythingPromptService.phpxphp api.php© AskAnythingPromptDto.phpD Events©AsKAnytningPromptservice.ongAsKAnytIngkepository.ongAskAnythingController.php© AutomatedReportsServiceTest.php© AskAnythingPromptDto.phpc Historyservice.ongD AskJiminnyAiWAWSBillingManagementu cache© AskJiminnyReportsController.php©AutomatedReportsService.php©) AskAnythingPromptServiceTest.phpclass AskAnychzngPromptserv1cepublic function _construct(private readonly broupreposttoryintertace sgroupkepository,) 4.3w countryD CustomerAp* drecucn arrau<askanuchinorrompcurosDatabaseDatadogDatettimeDeallnsightsN DealRisks• ElasticSearch• EloquentD Encodingpublic function getUser Suser. AskAnvthingPromptlarget Starget: arravspromots = Sthis->askanvthznaRepos1torv->f1ndPrompts8vUserAndtargetStargetLocal ChangesShelfConsoleChanges 7 files= .env.local app© ActivityController.php app/Http/Controllers/API© AskAnythingPromptService.phpapp/Component/AskAnything© AskAnythingPromptServiceTest.php tests/Unit/Component/AskAnything© AutomatedReportsServiceTest.php tests/Unit/Services/Kiosk/AutomatedReportTJP + → Side-by-side viewer -Do not ignoren202a0550apo/console/Commands/JiminnvDebuaCommand.ononamespace Jiminny Console Commands:use Illuminate\Console\Command;php loaaina.php confiause Jiminny Models\Team;Unversioned Files 9 files=.env.nikilocal appE.env.other app/*** Class JiminnyDebugCommandc) canAccessAlRevoris lest.ono tests/Unit/Policies©CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Re* @package Jiminny\Console\Commandsk tavicon.ico oublidE ids.txt appclass JiminnyDebugCommand extends Command1eraw sal querv sal apo© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspotprotected $signature = 'jiminny:debug';M. WEBHOOK FILTERING IMPLEMENTATION.md aoolpublic function handle(): voidSteam = Team: :find0rFail(2);SusersIds = $team->users() -›pluck('users.id') ->toArray();var dumo Susersids)*dnelexit(1);$0supoont Dally • nowU AutomatedReportsServiceTestv100% L28• Wed 20 May 15:01:04E custom.logA console [STAGING]E laravel.log4 SF jiminny@localhost] xC) CoachinaFeedbackCoachUserin.onpTx: AutovPlavaroundA HS_Jocal [jiminny@localhost]A console [PROD]# console [euyCascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 •fajiminny© Search.phpA2X2A~ 208021 A1 A18 V.2 Y6 ^atorromptservace-plpaelos . wilSELECT * FROM automated_reports where id = 67;SELECT * FROM automated reports where 10 = 421SELECT * FROM users WHERE id = 143; # group 28You're right to question this. Let me clarify:212select * from teams where id = 3143;select * from crm confiqurations where 1d = 500:select * from users where name = 'Integration Account'; # 1695SELE * FROM sochal accounts WHERE soctable 10 = 169511. The hasReports ParameterI actually identified this as required in my review — it was listed as "Still Open" issue #4. The edit() method was missingthe 8th parameter, causing the frontend to receive no has_reports field after an edit, which breaks the delete guard forpromots that nave reportsWithout it: After editing a prompt that backs a report, has_reports is missing frontend falls back to false → deleteView allmatedReports/# AutomatedReportsServiceTest.oho ÷28-1aon/Comoonent/AskAnvthina/MAskAnvthinaPromotService.onp218219220— 221select * from activities where erm confiquration id = 39and recording_state = 'recorded' and duration › 60and status = 'comoleted' and actual start time >=12025-12-011© Reject allAccept allSSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•S AdaptiveX ta B ?1differencescurrent versionnamespace Jiminny Console Commands:use Illuminate\Console\Command;use Illuminate\Support\Facades\Redis;use Jiminny\Models\Team;* Class LiminnyDebugCommand* @package Jiminny\Console\Commandsclass JiminnyDebugCommand extends Commandprotected Ssignature = 'jiminny:debug (action?}';public function handle(): void$action = $this->argument('action');if (Saction ==='redis-set') {Sthis->testRed1sSeto:return:exit(1):WN Windsurf Toams 27-8 UTF.8.f?4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62711
|
2222
|
3
|
2026-05-20T12:01:04.580647+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278464580_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEdit ViewSessionScriptsProfilesWindow iTerm2ShellEdit ViewSessionScriptsProfilesWindowHelp16|.APP (-zsh)DOCKER₴81DEV (-zsh)₴2APP (-zsh)Updating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.phpapp/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php273103714731402112+*app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.phpapp/Services/RecallAI/RecallAIService.php5311+++++++++front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests._/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DTOs/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallA1/RecallAlServiceTest.php60552416150++++++[PHONE] files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminskiandcontributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5690/5690 C8100%Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ ||Support Daily • nowA100% С8• Wed 20 May 15:01:04181Support Dailynow - 15:00-15:15= Notes - Support Daily.C Join Google MeetAPP...
|
NULL
|
8334918637671132099
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEdit ViewSessionScriptsProfilesWindow iTerm2ShellEdit ViewSessionScriptsProfilesWindowHelp16|.APP (-zsh)DOCKER₴81DEV (-zsh)₴2APP (-zsh)Updating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.phpapp/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php273103714731402112+*app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.phpapp/Services/RecallAI/RecallAIService.php5311+++++++++front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests._/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DTOs/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallA1/RecallAlServiceTest.php60552416150++++++[PHONE] files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminskiandcontributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5690/5690 C8100%Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ ||Support Daily • nowA100% С8• Wed 20 May 15:01:04181Support Dailynow - 15:00-15:15= Notes - Support Daily.C Join Google MeetAPP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62709
|
2222
|
2
|
2026-05-20T12:01:01.633445+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278461633_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","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}]...
|
5623854986745268125
|
2290957334813480527
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project...
|
62701
|
NULL
|
NULL
|
NULL
|
|
62710
|
2223
|
2
|
2026-05-20T12:01:01.614218+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278461614_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.39261967,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40259308,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.14684756,"width":0.38098404,"height":0.85315245},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
1561145305669319099
|
1126372201043003902
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}...
|
62704
|
NULL
|
NULL
|
NULL
|
|
62708
|
2223
|
1
|
2026-05-20T12:00:36.622992+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278436622_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.39261967,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40259308,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.14684756,"width":0.38098404,"height":0.85315245},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1544860027560100481
|
2290957334813480527
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
62704
|
NULL
|
NULL
|
NULL
|
|
62707
|
2222
|
1
|
2026-05-20T12:00:35.511596+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278435511_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","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}]...
|
-1544860027560100481
|
2290957334813480527
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
62701
|
NULL
|
NULL
|
NULL
|
|
62706
|
2223
|
0
|
2026-05-20T12:00:06.143293+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278406143_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.39261967,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40259308,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.14684756,"width":0.38098404,"height":0.85315245},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1544860027560100481
|
2290957334813480527
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
62704
|
NULL
|
NULL
|
NULL
|
|
62705
|
2222
|
0
|
2026-05-20T12:00:05.094697+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278405094_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","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}]...
|
-1544860027560100481
|
2290957334813480527
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
62701
|
NULL
|
NULL
|
NULL
|
|
62704
|
NULL
|
0
|
2026-05-20T11:59:35.721910+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278375721_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.39261967,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40259308,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.14684756,"width":0.38098404,"height":0.85315245},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1544860027560100481
|
2290957334813480527
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62703
|
NULL
|
0
|
2026-05-20T11:59:34.740217+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278374740_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","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}]...
|
-1544860027560100481
|
2290957334813480527
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
62701
|
NULL
|
NULL
|
NULL
|
|
62702
|
2221
|
20
|
2026-05-20T11:59:05.157677+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278345157_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.39261967,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.40259308,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"bounds":{"left":0.15990691,"top":0.14684756,"width":0.38098404,"height":0.85315245},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1544860027560100481
|
2290957334813480527
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
62699
|
NULL
|
NULL
|
NULL
|
|
62701
|
2220
|
15
|
2026-05-20T11:59:03.873316+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278343873_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\AskAnything;\n\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Contracts\\Repositories\\GroupRepositoryInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\AskAnything\\UserAskAnythingPrompt;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\UserRepository;\n\nclass AskAnythingPromptService\n{\n public function __construct(\n private readonly AskAnythingRepository $askAnythingRepository,\n private readonly UserRepository $userRepository,\n private readonly GroupRepositoryInterface $groupRepository,\n ) {\n }\n\n /**\n * @return array<AskAnythingPromptDto>\n */\n public function get(User $user, AskAnythingPromptTarget $target): array\n {\n $prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(\n $user,\n $target\n );\n\n $promptDtos = [];\n foreach ($prompts as $prompt) {\n $ownerUuid = null;\n if ($prompt->getOwner() !== null) {\n $ownerUuid = $prompt->getOwner()->getUuid();\n }\n $shareUsers = null;\n $shareGroups = null;\n\n // Provide users and groups only if owner of the prompt is current user\n if ($prompt->getOwnerId() === $user->getId()) {\n [$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);\n }\n\n $promptDtos[] = new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $ownerUuid,\n $shareUsers,\n $shareGroups,\n $prompt->getHasReports(),\n );\n }\n\n return $promptDtos;\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n */\n public function create(\n User $user,\n string $title,\n string $content,\n AskAnythingPromptTarget $target,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n $prompt = $this->askAnythingRepository->createPrompt(\n $user,\n $target,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n\n return new AskAnythingPromptDto(\n $prompt->getUuid(),\n $prompt->getTitle(),\n $prompt->getContent(),\n $target,\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n false,\n );\n }\n\n /**\n * @param array<string> $shareUsersUuids\n * @param array<string> $shareGroupsUuids\n *\n * @throws InvalidArgumentException\n */\n public function edit(\n AskAnythingPrompt $prompt,\n User $user,\n string $title,\n string $content,\n array $shareUsersUuids,\n array $shareGroupsUuids\n ): AskAnythingPromptDto {\n $shareUsers = $shareGroups = [];\n foreach ($shareUsersUuids as $userUuid) {\n $shareUsers[] = $this->userRepository->findByUuid($userUuid);\n }\n foreach ($shareGroupsUuids as $groupUuid) {\n $shareGroups[] = $this->groupRepository->findByUuid($groupUuid);\n }\n\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n\n $newPrompt = $this->askAnythingRepository->createPrompt(\n $user,\n $prompt->getTarget(),\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $newPrompt = $this->askAnythingRepository->editPrompt(\n $prompt,\n $title,\n $content,\n $shareUsers,\n $shareGroups,\n );\n } else {\n throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());\n }\n\n return new AskAnythingPromptDto(\n $newPrompt->getUuid(),\n $newPrompt->getTitle(),\n $newPrompt->getContent(),\n $newPrompt->getTarget(),\n $user->getUuid(),\n $shareUsersUuids,\n $shareGroupsUuids,\n $newPrompt->getHasReports(),\n );\n }\n\n public function delete(\n AskAnythingPrompt $prompt,\n User $user\n ): AskAnythingPrompt {\n if ($prompt->isDefaultPrompt()) {\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } elseif ($prompt->getOwnerId() === $user->getId()) {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $userPrompt?->delete();\n // For each relation, re-create the prompt for the receiver as their own\n $this->recreatePromptsForEachRelation($prompt);\n\n // Finally, delete the prompt\n $this->deletePromptIfNoRelations($prompt);\n } else {\n $userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);\n $groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);\n\n // If the prompt is shared directly with the user, then \"unshare\" it\n if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {\n $userPrompt->delete();\n $this->deletePromptIfNoRelations($prompt);\n } elseif ($groupPrompt instanceof UserAskAnythingPrompt) {\n // If the prompt is shared with the user and the group, then hide it for this user\n $this->askAnythingRepository->hidePromptForUser($prompt, $user);\n } else {\n throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());\n }\n }\n\n return $prompt;\n }\n\n public function reorder(\n User $user,\n array $promptUuids,\n ): void {\n foreach ($promptUuids as $index => $promptUuid) {\n $prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);\n $this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);\n }\n }\n\n /**\n * @param AskAnythingPrompt $prompt\n *\n * @return array[array<string>, array<string>]\n */\n private function getReceiverUuids(AskAnythingPrompt $prompt): array\n {\n $shareUsers = [];\n $shareGroups = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $shareUsers[] = $sharedUser->getUuid();\n }\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n $shareGroups[] = $sharedGroup->getUuid();\n }\n }\n\n return [$shareUsers, $shareGroups];\n }\n\n private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void\n {\n if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {\n // Disable and orphan any AutomatedReports that use this prompt\n $prompt->automatedReports()->withTrashed()->update([\n 'ask_anything_prompt_id' => null,\n 'status' => false,\n ]);\n\n // Delete only if there are no other relations to it.\n $this->askAnythingRepository->deletePrompt($prompt);\n }\n }\n\n private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void\n {\n [$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);\n $this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);\n }\n\n private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array\n {\n $sharedUsersIds = [];\n $sharedUsersRemovedIds = [];\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedUser = $sharedPrompt->getUser();\n if ($sharedPrompt->isRemoved() === true) {\n $sharedUsersRemovedIds[] = $sharedUser->getId();\n }\n if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {\n $sharedUsersIds[] = $sharedUser->getId();\n $this->askAnythingRepository->createPrompt(\n $sharedUser,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n $sharedPrompt->delete();\n }\n }\n\n return [$sharedUsersIds, $sharedUsersRemovedIds];\n }\n\n private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void\n {\n foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {\n $sharedGroup = $sharedPrompt->getGroup();\n if ($sharedGroup instanceof Group) {\n foreach ($sharedGroup->getMembers() as $member) {\n if (! in_array($member->getId(), $sharedUsersIds)\n && ! in_array($member->getId(), $sharedUsersRemovedIds)) {\n $sharedUsersIds[] = $member->getId();\n $this->askAnythingRepository->createPrompt(\n $member,\n $prompt->getTarget(),\n $prompt->getTitle(),\n $prompt->getContent(),\n [],\n [],\n );\n }\n }\n $sharedPrompt->delete();\n }\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","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}]...
|
-1544860027560100481
|
2290957334813480527
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Component\AskAnything;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Contracts\Repositories\GroupRepositoryInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AskAnything\UserAskAnythingPrompt;
use Jiminny\Models\Group;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\UserRepository;
class AskAnythingPromptService
{
public function __construct(
private readonly AskAnythingRepository $askAnythingRepository,
private readonly UserRepository $userRepository,
private readonly GroupRepositoryInterface $groupRepository,
) {
}
/**
* @return array<AskAnythingPromptDto>
*/
public function get(User $user, AskAnythingPromptTarget $target): array
{
$prompts = $this->askAnythingRepository->findPromptsByUserAndTarget(
$user,
$target
);
$promptDtos = [];
foreach ($prompts as $prompt) {
$ownerUuid = null;
if ($prompt->getOwner() !== null) {
$ownerUuid = $prompt->getOwner()->getUuid();
}
$shareUsers = null;
$shareGroups = null;
// Provide users and groups only if owner of the prompt is current user
if ($prompt->getOwnerId() === $user->getId()) {
[$shareUsers, $shareGroups] = $this->getReceiverUuids($prompt);
}
$promptDtos[] = new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$ownerUuid,
$shareUsers,
$shareGroups,
$prompt->getHasReports(),
);
}
return $promptDtos;
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*/
public function create(
User $user,
string $title,
string $content,
AskAnythingPromptTarget $target,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
$prompt = $this->askAnythingRepository->createPrompt(
$user,
$target,
$title,
$content,
$shareUsers,
$shareGroups,
);
return new AskAnythingPromptDto(
$prompt->getUuid(),
$prompt->getTitle(),
$prompt->getContent(),
$target,
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
false,
);
}
/**
* @param array<string> $shareUsersUuids
* @param array<string> $shareGroupsUuids
*
* @throws InvalidArgumentException
*/
public function edit(
AskAnythingPrompt $prompt,
User $user,
string $title,
string $content,
array $shareUsersUuids,
array $shareGroupsUuids
): AskAnythingPromptDto {
$shareUsers = $shareGroups = [];
foreach ($shareUsersUuids as $userUuid) {
$shareUsers[] = $this->userRepository->findByUuid($userUuid);
}
foreach ($shareGroupsUuids as $groupUuid) {
$shareGroups[] = $this->groupRepository->findByUuid($groupUuid);
}
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
$newPrompt = $this->askAnythingRepository->createPrompt(
$user,
$prompt->getTarget(),
$title,
$content,
$shareUsers,
$shareGroups,
);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$newPrompt = $this->askAnythingRepository->editPrompt(
$prompt,
$title,
$content,
$shareUsers,
$shareGroups,
);
} else {
throw new InvalidArgumentException('Edit not allowed for prompt ' . $prompt->getUuid());
}
return new AskAnythingPromptDto(
$newPrompt->getUuid(),
$newPrompt->getTitle(),
$newPrompt->getContent(),
$newPrompt->getTarget(),
$user->getUuid(),
$shareUsersUuids,
$shareGroupsUuids,
$newPrompt->getHasReports(),
);
}
public function delete(
AskAnythingPrompt $prompt,
User $user
): AskAnythingPrompt {
if ($prompt->isDefaultPrompt()) {
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} elseif ($prompt->getOwnerId() === $user->getId()) {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$userPrompt?->delete();
// For each relation, re-create the prompt for the receiver as their own
$this->recreatePromptsForEachRelation($prompt);
// Finally, delete the prompt
$this->deletePromptIfNoRelations($prompt);
} else {
$userPrompt = $this->askAnythingRepository->findSharedPromptByUser($prompt->getId(), $user);
$groupPrompt = $this->askAnythingRepository->findSharedPromptByUserGroup($prompt->getId(), $user);
// If the prompt is shared directly with the user, then "unshare" it
if ($userPrompt instanceof UserAskAnythingPrompt && $groupPrompt === null) {
$userPrompt->delete();
$this->deletePromptIfNoRelations($prompt);
} elseif ($groupPrompt instanceof UserAskAnythingPrompt) {
// If the prompt is shared with the user and the group, then hide it for this user
$this->askAnythingRepository->hidePromptForUser($prompt, $user);
} else {
throw new InvalidArgumentException('Unknown delete allowance for prompt ' . $prompt->getUuid());
}
}
return $prompt;
}
public function reorder(
User $user,
array $promptUuids,
): void {
foreach ($promptUuids as $index => $promptUuid) {
$prompt = $this->askAnythingRepository->getPromptByUuid($promptUuid);
$this->askAnythingRepository->orderPromptForUser($prompt, $user, $index + 1);
}
}
/**
* @param AskAnythingPrompt $prompt
*
* @return array[array<string>, array<string>]
*/
private function getReceiverUuids(AskAnythingPrompt $prompt): array
{
$shareUsers = [];
$shareGroups = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedUser instanceof User && $sharedUser->isStatusActive()) {
$shareUsers[] = $sharedUser->getUuid();
}
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
$shareGroups[] = $sharedGroup->getUuid();
}
}
return [$shareUsers, $shareGroups];
}
private function deletePromptIfNoRelations(AskAnythingPrompt $prompt): void
{
if ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId())->isEmpty()) {
// Disable and orphan any AutomatedReports that use this prompt
$prompt->automatedReports()->withTrashed()->update([
'ask_anything_prompt_id' => null,
'status' => false,
]);
// Delete only if there are no other relations to it.
$this->askAnythingRepository->deletePrompt($prompt);
}
}
private function recreatePromptsForEachRelation(AskAnythingPrompt $prompt): void
{
[$sharedUsersIds, $sharedUsersRemovedIds] = $this->recreatePromptsForUserRelations($prompt);
$this->recreatePromptsForGroupRelations($prompt, $sharedUsersIds, $sharedUsersRemovedIds);
}
private function recreatePromptsForUserRelations(AskAnythingPrompt $prompt): array
{
$sharedUsersIds = [];
$sharedUsersRemovedIds = [];
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedUser = $sharedPrompt->getUser();
if ($sharedPrompt->isRemoved() === true) {
$sharedUsersRemovedIds[] = $sharedUser->getId();
}
if ($sharedPrompt->isRemoved() !== true && $sharedUser instanceof User && $sharedUser->isStatusActive()) {
$sharedUsersIds[] = $sharedUser->getId();
$this->askAnythingRepository->createPrompt(
$sharedUser,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
$sharedPrompt->delete();
}
}
return [$sharedUsersIds, $sharedUsersRemovedIds];
}
private function recreatePromptsForGroupRelations(AskAnythingPrompt $prompt, array $sharedUsersIds, array $sharedUsersRemovedIds): void
{
foreach ($this->askAnythingRepository->findSharedUsersAndGroupsByPromptId($prompt->getId()) as $sharedPrompt) {
$sharedGroup = $sharedPrompt->getGroup();
if ($sharedGroup instanceof Group) {
foreach ($sharedGroup->getMembers() as $member) {
if (! in_array($member->getId(), $sharedUsersIds)
&& ! in_array($member->getId(), $sharedUsersRemovedIds)) {
$sharedUsersIds[] = $member->getId();
$this->askAnythingRepository->createPrompt(
$member,
$prompt->getTarget(),
$prompt->getTitle(),
$prompt->getContent(),
[],
[],
);
}
}
$sharedPrompt->delete();
}
}
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
21
1
18
2
6
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003
SELECT * FROM activities WHERE id IN (16,422003);
SELECT * FROM activities where status = 'failed';
SELECT * FROM tracks WHERE activity_id = 422003;
SELECT
a.*
FROM activities a
JOIN users u ON a.user_id = u.id
WHERE
a.status = 'completed'
AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid
AND a.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.activity_id = a.id
AND t.type IN ('audio', 'video')
)
ORDER BY a.actual_start_time DESC
LIMIT 25;
select * from teams where id = 19;
select * from crm_configurations where provider = 'pipedrive';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 19 and sa.provider = 'pipedrive';
SELECT * FROM social_accounts WHERE id = 1116;
UPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',
provider_refresh_token = '5034113:[TELEGRAM_TOKEN]b2bfc',
expires = 1779091997,
state = 'connected'
WHERE id = 1116;
"provider_user_token": "v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA",
"provider_refresh_token": "5034113:[TELEGRAM_TOKEN]b2bfc",
"expires": 1779091997,
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62700
|
2220
|
14
|
2026-05-20T11:59:02.289147+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278342289_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4769499369406844942
|
-7845270547046784059
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpAPP (docker)DOCKERO ₴1DEV (-zsh)₴2APP (docker)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-› origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue->origin/JY-20808-schedule-reset-on-low-queue* [new branch]JY-20930-ringcentral-disconnected-crmorigin/JY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6master->origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++*app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.php+*app/Component/ES/Processor/DTOs/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.phpapp/Services/RecallAI/RecallAIService.php2731037147314021126+++-5311front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests_/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAlServiceTest.php6055241615093863204424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminskiandcontributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loaded config default from ".php-cs-fixer.dist.php"# Support Daily - in 1 m83100% С8• Wed 20 May 14:59:01screenpipe"T81O ₴4APP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62699
|
2221
|
19
|
2026-05-20T11:59:01.312267+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278341312_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
74
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => ...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.375,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.38663563,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"74","depth":4,"bounds":{"left":0.4005984,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testUpdateAskJiminnyReportStatusSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(true);\n\n $mockFreshReport = $this->createMock(AutomatedReport::class);\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => true])\n ->willReturn($mockReport);\n $mockReport->expects($this->once())\n ->method('fresh')\n ->willReturn($mockFreshReport);\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $result = $service->updateAskJiminnyReportStatus($mockReport, true);\n\n $this->assertSame($mockFreshReport, $result);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testUpdateAskJiminnyReportStatusSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(true);\n\n $mockFreshReport = $this->createMock(AutomatedReport::class);\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => true])\n ->willReturn($mockReport);\n $mockReport->expects($this->once())\n ->method('fresh')\n ->willReturn($mockFreshReport);\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $result = $service->updateAskJiminnyReportStatus($mockReport, true);\n\n $this->assertSame($mockFreshReport, $result);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2217433079836078976
|
-1412888448243747103
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
74
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62698
|
2221
|
18
|
2026-05-20T11:58:58.219318+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278338219_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormINavigarecodeWindowFV faVsco.js#12098 on J PhostormINavigarecodeWindowFV faVsco.js#12098 on JY-20676-delete-report-related-olProledeyC ActivityController.ong© AskAnythingController.php> • OpportunitySyncStrategy• • ProspectSearchStrategy• • ServiceTraits© ClientTest.phpc) DecorateActivity lest.onpC DeleteObjectsTraitTest.php© FieldDefinitionsTest.phpC GetActivityFieldNameTest.phpPayloadBuilderTest.php@ QueryBuilderTest.phpC) QueryHandlerTest.php© QuerylteratorTest.php© QuervResultsTests.phpC ServiceTest.phpC SvncBatchRedisServiceTest.php© AskAnythingPromptService.php© AutomatedReportsServiceTest.php X©ASKAnythingPromptDto.png©) BaseServiceTest.oho©) CachedcrmServiceDecoratorTest.ohr©) CrmActivitvServicelest.onoc) crmcontidurationSettinasServicetiestc) crmobiectsResolveriiest.onoc DefaultProsnectSearchStrateavTest.cc EmailHelperTest.ohoc) FieldValueConverterTest.ohoc LavoutManaderTest.oho© MigrateProviderServiceTest.php© OpportunityActivityMatcherTest.php© OpportunitySyncStrategyResolverTes© ProspectCacheTest.php© ProspectSearchStrategyFactoryTest.pC ProviderPReaistrvTect nhnl© RecordSelectorTest.phpoDocolwoCamnanyNamoRySmailTroitt.©) TimePeriodlteratorTest.php© UpdateCrmDataResolverTest.php> C Internalv @ Kioskv @ AutomatedReports© ActivitvTvpeServiceTest.php© AskJiminnyReportActivityServic© AskJiminnyReportServiceTest.pl@ AutomatedReportsCallbackServiC AutomatedReportsServiceActiviC) AutomatedRenortsServiceExten@ AutomatedReportsServiceFieldi(C) AutomatedRenortsServiceFormal@ AutomatedReportsServiceGetAdc) AutomatedRenortsServiceGetPla(ci AutomatedRenortsServiceRenorci AutomatedRenortsServiceTect nhnch AutomatedRenortsServiceValidateCh AutomatedRenortsServiceValidatioAskJiminnyreporiscontroller.ong©) AskAnythingPromptServiceTest.phpdeclare(strict_types=1);namesoace lests Unit Services Klosk AutomatedRevorts:> use..class AutomatedReportsServicetest extends Testcase88 usagesprivate AutomatedReportsService Sservice;SAOTSprotected function setUp: void{...H83 0 >protected function tearDown: void{...}28 usagesprivate function getService(Smосkuserkeposicory = null$mockStageRepository = nullSmockleamkepos1tory = nullSmоckkepo = null): AutomatedReportsService {...}14 Y#|DataProvider('transtormMed1alypesbataProv1der'public function testTransformMediaTvpes(arrav SmediaTvpes, array Sexpected): voids...?126 0public function testGetMediaTvpeFieldDataWithoutReport@: voidSresult = sthis->service->oetMediaTvoerieldDatad report: nul0:public function testUodateAskJiminnvReportStatusNotAskJiminnv@: voidalClassName: AutomatedRenort::class):SmockReport-›expects(Sthis-›once())->method( constraint:"isAskJiminnvRenort')->willReturn( vaSservice = new AutomatedRenontsSenvicedTeamPenositonv.:clase).GnounRenository..clase)public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}#[DataProvider('hasCallTypeDialerDataProvider')]-renort-rolated-dhioctsl hac heon rectored /l Pollback II Confiaure (15 minutes ado)custom.log= laravel.log4 SF [jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING)© Search.php207A 14 A 102 V 74 %v 208= 211212=214—211= 211=217-218=219=220=222=223224225=22228=229230• 23÷= 232— 27₴-234235236237238239=240241242244245- 249— 250© CoachingFeedbackCoachUserln.phpTx: AutovriaygroundSo jiminny v021 A1 A18 V.2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account'; # 1695SELECT * FROMsocial accounts WHERE sociable id = 1695:select * from activities where crm confiquration id = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;splect * from leads.SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003):SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;csisoTFROM activities aJOIN users u 1..n<->1: ON a.user_id = u.idWHEREa.status = 'completed'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks +WHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESdLTMTT 25:select * from teams whene id = 10-select * from erm confiqunations where nnoviden = Ininednive!.SEIECTCONCATEN #d CASE WHEN n id - + ounon #d THEN 1 (ounon)! EISE 11 ENO) AS ncon ¿dl+ ounon id EPOM cocial Accountc caOTM ucone mon mid = ca cociablo id• suppont Dally • In 2 m100% L2• Wed 20 May 14:58:58CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..BActivaityoatorromptservace-plpaelos . wilYou're right to question this. Let me clarify:1. The hasReports ParameterI actually identified this as required in my review — it was listed as "Still Open" issue #4. The edit() method was missingthe 8th parameter, causina the frontend to receive no has reports field after an edit. which breaks the delete quard forpromots that nave reporsWithout it: After editina a oromot that backs a report. has reports is missina → frontend talls back to false → deleteZ. Pertormance of loadCount('automatedReports')Imnact. Minimal - it eyecutes one additionall colNt(*) query.SFIECT GOUNT(*) FROM automated renortc WHFRF activity cearch id = ?• Indexed query — assumes activity search 1d has an index (Standara tor toreign keys)• • Consistent with existing code — ListActivitvSearcho already uses withCount ('automatedReports') line 1100\• • Only on single fetch - fetchActivitySearch is for viewing one search, not list operations• V No N+1 — it's a sinqle querv. not per-itemAlternative considered: Eager loading the actual reports with load('automatedReports') would be heavier. loadCount()is the liahtweiaht ontion — it only aets the count. not the fulli modelsVerdict: The performance impact is negligible (~1-2ms for an indexed count), and it's necessary for correctness. The bug itfixes (delete quard silently failina) outweiahs the minimal query costRenorts/M AutomatedRenortcServiceTest.nhn +28-11:/M.AskAnvthinaPromotService.ohvAsk anvthing 884-D)< CodeC° Adantive* Reiect alliiAccent allW Windsurf Teamc1-1.UTE.8io 4 spaces...
|
NULL
|
-5416708063108523095
|
NULL
|
click
|
ocr
|
NULL
|
PhostormINavigarecodeWindowFV faVsco.js#12098 on J PhostormINavigarecodeWindowFV faVsco.js#12098 on JY-20676-delete-report-related-olProledeyC ActivityController.ong© AskAnythingController.php> • OpportunitySyncStrategy• • ProspectSearchStrategy• • ServiceTraits© ClientTest.phpc) DecorateActivity lest.onpC DeleteObjectsTraitTest.php© FieldDefinitionsTest.phpC GetActivityFieldNameTest.phpPayloadBuilderTest.php@ QueryBuilderTest.phpC) QueryHandlerTest.php© QuerylteratorTest.php© QuervResultsTests.phpC ServiceTest.phpC SvncBatchRedisServiceTest.php© AskAnythingPromptService.php© AutomatedReportsServiceTest.php X©ASKAnythingPromptDto.png©) BaseServiceTest.oho©) CachedcrmServiceDecoratorTest.ohr©) CrmActivitvServicelest.onoc) crmcontidurationSettinasServicetiestc) crmobiectsResolveriiest.onoc DefaultProsnectSearchStrateavTest.cc EmailHelperTest.ohoc) FieldValueConverterTest.ohoc LavoutManaderTest.oho© MigrateProviderServiceTest.php© OpportunityActivityMatcherTest.php© OpportunitySyncStrategyResolverTes© ProspectCacheTest.php© ProspectSearchStrategyFactoryTest.pC ProviderPReaistrvTect nhnl© RecordSelectorTest.phpoDocolwoCamnanyNamoRySmailTroitt.©) TimePeriodlteratorTest.php© UpdateCrmDataResolverTest.php> C Internalv @ Kioskv @ AutomatedReports© ActivitvTvpeServiceTest.php© AskJiminnyReportActivityServic© AskJiminnyReportServiceTest.pl@ AutomatedReportsCallbackServiC AutomatedReportsServiceActiviC) AutomatedRenortsServiceExten@ AutomatedReportsServiceFieldi(C) AutomatedRenortsServiceFormal@ AutomatedReportsServiceGetAdc) AutomatedRenortsServiceGetPla(ci AutomatedRenortsServiceRenorci AutomatedRenortsServiceTect nhnch AutomatedRenortsServiceValidateCh AutomatedRenortsServiceValidatioAskJiminnyreporiscontroller.ong©) AskAnythingPromptServiceTest.phpdeclare(strict_types=1);namesoace lests Unit Services Klosk AutomatedRevorts:> use..class AutomatedReportsServicetest extends Testcase88 usagesprivate AutomatedReportsService Sservice;SAOTSprotected function setUp: void{...H83 0 >protected function tearDown: void{...}28 usagesprivate function getService(Smосkuserkeposicory = null$mockStageRepository = nullSmockleamkepos1tory = nullSmоckkepo = null): AutomatedReportsService {...}14 Y#|DataProvider('transtormMed1alypesbataProv1der'public function testTransformMediaTvpes(arrav SmediaTvpes, array Sexpected): voids...?126 0public function testGetMediaTvpeFieldDataWithoutReport@: voidSresult = sthis->service->oetMediaTvoerieldDatad report: nul0:public function testUodateAskJiminnvReportStatusNotAskJiminnv@: voidalClassName: AutomatedRenort::class):SmockReport-›expects(Sthis-›once())->method( constraint:"isAskJiminnvRenort')->willReturn( vaSservice = new AutomatedRenontsSenvicedTeamPenositonv.:clase).GnounRenository..clase)public function testHasCallTypeConference(array $callTypes, bool Sexpected): void{...}#[DataProvider('hasCallTypeDialerDataProvider')]-renort-rolated-dhioctsl hac heon rectored /l Pollback II Confiaure (15 minutes ado)custom.log= laravel.log4 SF [jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING)© Search.php207A 14 A 102 V 74 %v 208= 211212=214—211= 211=217-218=219=220=222=223224225=22228=229230• 23÷= 232— 27₴-234235236237238239=240241242244245- 249— 250© CoachingFeedbackCoachUserln.phpTx: AutovriaygroundSo jiminny v021 A1 A18 V.2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account'; # 1695SELECT * FROMsocial accounts WHERE sociable id = 1695:select * from activities where crm confiquration id = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;splect * from leads.SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003):SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;csisoTFROM activities aJOIN users u 1..n<->1: ON a.user_id = u.idWHEREa.status = 'completed'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks +WHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESdLTMTT 25:select * from teams whene id = 10-select * from erm confiqunations where nnoviden = Ininednive!.SEIECTCONCATEN #d CASE WHEN n id - + ounon #d THEN 1 (ounon)! EISE 11 ENO) AS ncon ¿dl+ ounon id EPOM cocial Accountc caOTM ucone mon mid = ca cociablo id• suppont Dally • In 2 m100% L2• Wed 20 May 14:58:58CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..BActivaityoatorromptservace-plpaelos . wilYou're right to question this. Let me clarify:1. The hasReports ParameterI actually identified this as required in my review — it was listed as "Still Open" issue #4. The edit() method was missingthe 8th parameter, causina the frontend to receive no has reports field after an edit. which breaks the delete quard forpromots that nave reporsWithout it: After editina a oromot that backs a report. has reports is missina → frontend talls back to false → deleteZ. Pertormance of loadCount('automatedReports')Imnact. Minimal - it eyecutes one additionall colNt(*) query.SFIECT GOUNT(*) FROM automated renortc WHFRF activity cearch id = ?• Indexed query — assumes activity search 1d has an index (Standara tor toreign keys)• • Consistent with existing code — ListActivitvSearcho already uses withCount ('automatedReports') line 1100\• • Only on single fetch - fetchActivitySearch is for viewing one search, not list operations• V No N+1 — it's a sinqle querv. not per-itemAlternative considered: Eager loading the actual reports with load('automatedReports') would be heavier. loadCount()is the liahtweiaht ontion — it only aets the count. not the fulli modelsVerdict: The performance impact is negligible (~1-2ms for an indexed count), and it's necessary for correctness. The bug itfixes (delete quard silently failina) outweiahs the minimal query costRenorts/M AutomatedRenortcServiceTest.nhn +28-11:/M.AskAnvthinaPromotService.ohvAsk anvthing 884-D)< CodeC° Adantive* Reiect alliiAccent allW Windsurf Teamc1-1.UTE.8io 4 spaces...
|
62697
|
NULL
|
NULL
|
NULL
|
|
62697
|
2221
|
17
|
2026-05-20T11:58:54.657529+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278334657_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => ...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.375,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.38663563,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","depth":4,"bounds":{"left":0.4005984,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testUpdateAskJiminnyReportStatusSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(true);\n\n $mockFreshReport = $this->createMock(AutomatedReport::class);\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => true])\n ->willReturn($mockReport);\n $mockReport->expects($this->once())\n ->method('fresh')\n ->willReturn($mockFreshReport);\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $result = $service->updateAskJiminnyReportStatus($mockReport, true);\n\n $this->assertSame($mockFreshReport, $result);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Carbon as IlluminateCarbon;\nuse Illuminate\\Contracts\\Bus\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Storage;\nuse Jiminny\\Component\\AskAnything\\AskAnythingPromptService;\nuse Jiminny\\Component\\AskAnything\\Dtos\\AskAnythingPromptDto;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Contracts\\Repositories\\PlaybookCategoryRepository;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\ModelNotFoundException;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPrompt;\nuse Jiminny\\Models\\AskAnything\\AskAnythingPromptTarget;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Partner;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\AskAnythingRepository;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Repositories\\GroupRepository;\nuse Jiminny\\Repositories\\SearchRepository;\nuse Jiminny\\Repositories\\StageRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ActivityTypeService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\DealStagesService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\RecipientsService;\nuse Mockery;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\n\nclass AutomatedReportsServiceTest extends TestCase\n{\n private AutomatedReportsService $service;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n // Create a real instance of the service without calling the constructor\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $this->service = $reflection->newInstanceWithoutConstructor();\n\n // Manually set the dependencies using reflection\n $dependencies = [\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ];\n\n foreach ($dependencies as $propertyName => $class) {\n $property = $reflection->getProperty($propertyName);\n $property->setAccessible(true);\n $property->setValue($this->service, $this->createMock($class));\n }\n }\n\n protected function tearDown(): void\n {\n parent::tearDown();\n Mockery::close();\n }\n\n private function getService(\n $mockUserRepository = null,\n $mockStageRepository = null,\n $mockTeamRepository = null,\n $mockRepo = null,\n ): AutomatedReportsService {\n return new AutomatedReportsService(\n ($mockTeamRepository ?? $this->createMock(TeamRepository::class)),\n $this->createMock(GroupRepository::class),\n ($mockUserRepository ?? $this->createMock(UserRepository::class)),\n ($mockStageRepository ?? $this->createMock(StageRepository::class)),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n ($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n }\n\n #[DataProvider('transformMediaTypesDataProvider')]\n public function testTransformMediaTypes(array $mediaTypes, array $expected): void\n {\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformMediaTypes');\n\n $result = $method->invoke($this->service, $report);\n\n $this->assertEquals($expected, $result);\n }\n\n public function testGetMediaTypeFieldDataWithoutReport(): void\n {\n $result = $this->service->getMediaTypeFieldData(null);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEmpty($result['value']);\n $this->assertEquals('media_types', $result['id']);\n }\n\n public function testGetMediaTypeFieldDataWithReport(): void\n {\n $mediaTypes = ['pdf', 'podcast'];\n $report = new AutomatedReport(['media_types' => $mediaTypes]);\n\n $result = $this->service->getMediaTypeFieldData($report);\n\n $expectedValue = [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ];\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('value', $result);\n $this->assertEquals($expectedValue, $result['value']);\n }\n\n public static function transformMediaTypesDataProvider(): array\n {\n return [\n 'empty array' => [\n 'mediaTypes' => [],\n 'expected' => [],\n ],\n 'pdf only' => [\n 'mediaTypes' => ['pdf'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ],\n ],\n 'podcast only' => [\n 'mediaTypes' => ['podcast'],\n 'expected' => [\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'both pdf and podcast' => [\n 'mediaTypes' => ['pdf', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n 'with invalid type' => [\n 'mediaTypes' => ['pdf', 'invalid', 'podcast'],\n 'expected' => [\n ['id' => 'pdf', 'name' => 'PDF'],\n ['id' => 'podcast', 'name' => 'Podcast'],\n ],\n ],\n ];\n }\n\n #[DataProvider('hasCallTypeConferenceDataProvider')]\n public function testHasCallTypeConference(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeConference($report);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('hasCallTypeDialerDataProvider')]\n public function testHasCallTypeDialer(array $callTypes, bool $expected): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getCallTypes')->willReturn($callTypes);\n\n $result = $this->service->hasCallTypeDialer($report);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function hasCallTypeConferenceDataProvider(): array\n {\n return [\n 'has conference' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have conference' => [\n 'callTypes' => ['dialer', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public static function hasCallTypeDialerDataProvider(): array\n {\n return [\n 'has dialer' => [\n 'callTypes' => ['conference', 'dialer'],\n 'expected' => true,\n ],\n 'does not have dialer' => [\n 'callTypes' => ['conference', 'other'],\n 'expected' => false,\n ],\n 'empty call types' => [\n 'callTypes' => [],\n 'expected' => false,\n ],\n ];\n }\n\n public function testTransformReportResultsWithEmptyCollection(): void\n {\n $emptyCollection = new Collection([]);\n\n $result = $this->service->transformReportResults($emptyCollection);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testTransformReportResultsStructure(): void\n {\n // Create a mock AutomatedReportResult with minimal setup to test structure\n $mockReportResult = $this->createMockReportResult();\n $collection = new Collection([$mockReportResult]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(1, $result);\n\n $transformedResult = $result[0];\n\n // Verify all expected keys are present\n $expectedKeys = [\n 'id', 'name', 'frequency', 'recipients',\n 'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',\n ];\n\n foreach ($expectedKeys as $key) {\n $this->assertArrayHasKey($key, $transformedResult);\n }\n\n // Verify structure of nested arrays\n $this->assertIsArray($transformedResult['frequency']);\n $this->assertArrayHasKey('id', $transformedResult['frequency']);\n $this->assertArrayHasKey('name', $transformedResult['frequency']);\n\n $this->assertIsArray($transformedResult['report_type']);\n $this->assertArrayHasKey('id', $transformedResult['report_type']);\n $this->assertArrayHasKey('name', $transformedResult['report_type']);\n\n $this->assertIsArray($transformedResult['recipients']);\n\n // Verify TODO fields are null as expected\n $this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);\n $this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);\n $this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);\n }\n\n public function testTransformReportResultsWithMultipleResults(): void\n {\n $mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');\n $mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');\n $collection = new Collection([$mockReportResult1, $mockReportResult2]);\n\n $result = $this->service->transformReportResults($collection);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n\n // Verify different UUIDs\n $this->assertEquals('result-uuid-1', $result[0]['id']);\n $this->assertEquals('result-uuid-2', $result[1]['id']);\n\n // Verify both results have the expected structure\n foreach ($result as $transformedResult) {\n $this->assertArrayHasKey('id', $transformedResult);\n $this->assertArrayHasKey('name', $transformedResult);\n $this->assertArrayHasKey('frequency', $transformedResult);\n $this->assertArrayHasKey('recipients', $transformedResult);\n $this->assertArrayHasKey('report_type', $transformedResult);\n }\n }\n\n #[DataProvider('isUserRecipientOfReportDataProvider')]\n public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn(null);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertEquals($expected, $result);\n }\n\n #[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]\n public function testIsUserRecipientOfAskJiminnyReportViaGroup(\n int $userId,\n ?int $groupId,\n array $recipients,\n array $reportGroups,\n bool $expected,\n ): void {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn($userId);\n $mockUser->method('getGroupId')->willReturn($groupId);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n $mockReport->method('isAskJiminnyReport')->willReturn(true);\n $mockReport->method('getGroups')->willReturn($reportGroups);\n\n $this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void\n {\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n $mockUser->method('getGroupId')->willReturn(5);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('isAskJiminnyReport')->willReturn(false);\n $mockReport->method('getGroups')->willReturn([5]);\n\n $this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));\n }\n\n public static function isUserRecipientOfAskJiminnyReportDataProvider(): array\n {\n return [\n 'group member - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 7,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => true,\n ],\n 'group mismatch - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => 9,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7, 8],\n 'expected' => false,\n ],\n 'user with no group - ask jiminny' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => []],\n 'reportGroups' => [7],\n 'expected' => false,\n ],\n 'recipient users take precedence over group' => [\n 'userId' => 123,\n 'groupId' => null,\n 'recipients' => ['users' => [123]],\n 'reportGroups' => [],\n 'expected' => true,\n ],\n ];\n }\n\n public function testIsUserRecipientOfReportWithEmptyRecipients(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with no recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public function testIsUserRecipientOfReportWithNoUsersKey(): void\n {\n // Create mock User\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getId')->willReturn(123);\n\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);\n\n $result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);\n\n $this->assertFalse($result);\n }\n\n public static function isUserRecipientOfReportDataProvider(): array\n {\n return [\n 'user is recipient - single user' => [\n 'userId' => 123,\n 'recipients' => ['users' => [123]],\n 'expected' => true,\n ],\n 'user is recipient - multiple users' => [\n 'userId' => 456,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => true,\n ],\n 'user is not recipient - single user' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123]],\n 'expected' => false,\n ],\n 'user is not recipient - multiple users' => [\n 'userId' => 999,\n 'recipients' => ['users' => [123, 456, 789]],\n 'expected' => false,\n ],\n 'user is recipient - string IDs converted to int' => [\n 'userId' => 123,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => true,\n ],\n 'user is not recipient - string IDs converted to int' => [\n 'userId' => 999,\n 'recipients' => ['users' => ['123', '456']],\n 'expected' => false,\n ],\n 'empty users array' => [\n 'userId' => 123,\n 'recipients' => ['users' => []],\n 'expected' => false,\n ],\n ];\n }\n\n private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult\n {\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $mockReport->method('getGroups')->willReturn([10, 20]);\n $mockReport->method('getType')->willReturn($reportType);\n\n // Create mock Team\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock Group\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-10');\n $mockGroup->method('getName')->willReturn('Test Team');\n\n $mockQueryBuilder = Mockery::mock();\n $mockQueryBuilder->shouldReceive('where')->andReturnSelf();\n $mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);\n\n $dataRelation = Mockery::mock(HasMany::class);\n $dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);\n $dataRelation->shouldReceive('get')->andReturn(\n new \\Illuminate\\Database\\Eloquent\\Collection([$mockGroup])\n );\n\n $mockTeam->method('groups')->willReturn($dataRelation);\n $mockReport->method('getTeam')->willReturn($mockTeam);\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n $mockReportResult->method('getUuid')->willReturn($uuid);\n $mockReportResult->method('getGeneratedAt')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15T10:30:00Z')\n );\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n\n // Mock methods used in getReportFileName\n $mockReportResult->method('getReportType')->willReturn($reportType);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-08')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2024-01-15')\n );\n $mockReportResult->method('getGroups')->willReturn([10]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n return $mockReportResult;\n }\n\n #[DataProvider('getUsersUuidsDataProvider')]\n public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void\n {\n // Create mock UserRepository\n $mockUserRepository = $this->createMock(UserRepository::class);\n\n // Configure the mock to return specific users for specific IDs using a callback\n $mockUserRepository->method('find')\n ->willReturnCallback(function ($userId) use ($mockUsers) {\n if (! isset($mockUsers[$userId])) {\n return null;\n }\n\n $userUuid = $mockUsers[$userId]['uuid'] ?? null;\n\n if ($userUuid === null) {\n return null;\n }\n\n $mockUser = $this->createMock(\\Jiminny\\Models\\User::class);\n $mockUser->method('getUuid')->willReturn((string) $userUuid);\n\n return $mockUser;\n });\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn($recipients);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetUsersUuidsWithEmptyRecipients(): void\n {\n // Create mock AutomatedReport with empty recipients\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn([]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNoUsersKey(): void\n {\n // Create mock AutomatedReport with recipients but no 'users' key\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);\n\n $result = $this->service->getUsersUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetUsersUuidsWithNonExistentUsers(): void\n {\n // Create mock UserRepository that returns null for all users\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn(null);\n\n // Create service with mocked UserRepository\n $automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);\n\n $result = $automatedReportsService->getUsersUuids($mockReport);\n\n // Should return array with null values for non-existent users\n $this->assertEquals([], $result);\n }\n\n public static function getUsersUuidsDataProvider(): array\n {\n return [\n 'single user found' => [\n 'recipients' => ['users' => [123]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n ],\n 'expectedUuids' => ['user-uuid-123'],\n ],\n 'multiple users found' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n 456 => ['id' => 456, 'uuid' => 'user-uuid-456'],\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],\n ],\n 'mixed found and not found users' => [\n 'recipients' => ['users' => [123, 456, 789]],\n 'mockUsers' => [\n 123 => ['id' => 123, 'uuid' => 'user-uuid-123'],\n // 456 not found in DB\n 789 => ['id' => 789, 'uuid' => 'user-uuid-789'],\n ],\n 'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out\n ],\n 'empty users array' => [\n 'recipients' => ['users' => []],\n 'mockUsers' => [],\n 'expectedUuids' => [],\n ],\n 'all users not found' => [\n 'recipients' => ['users' => [123, 456]],\n 'mockUsers' => [], // No users found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getCurrentDealStagesUuidsDataProvider')]\n public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void\n {\n // Create mock StageRepository\n $mockStageRepository = $this->createMock(StageRepository::class);\n\n // Configure the mock to return specific stages for specific IDs using a callback\n $mockStageRepository->method('find')\n ->willReturnCallback(function ($stageId) use ($mockStages) {\n if (! isset($mockStages[$stageId])) {\n return null;\n }\n\n $stageUuid = $mockStages[$stageId]['uuid'] ?? null;\n\n if ($stageUuid === null) {\n return null;\n }\n\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn((string) $stageUuid);\n\n return $mockStage;\n });\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals($expectedUuids, $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithEmptyStages(): void\n {\n // Create mock AutomatedReport with empty current deal stages\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n\n $result = $this->service->getCurrentDealStagesUuids($mockReport);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void\n {\n // Create mock StageRepository that returns null for all stages\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturn(null);\n\n // Create service with mocked StageRepository\n $automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);\n\n $result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);\n\n // Should return array with null values for non-existent stages\n $this->assertEquals([], $result);\n }\n\n public static function getCurrentDealStagesUuidsDataProvider(): array\n {\n return [\n 'single stage found' => [\n 'currentDealStages' => [10],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n ],\n 'expectedUuids' => ['stage-uuid-10'],\n ],\n 'multiple stages found' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n 20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],\n ],\n 'mixed found and not found stages' => [\n 'currentDealStages' => [10, 20, 30],\n 'mockStages' => [\n 10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],\n // 20 not found in DB\n 30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],\n ],\n 'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out\n ],\n 'empty stages array' => [\n 'currentDealStages' => [],\n 'mockStages' => [],\n 'expectedUuids' => [],\n ],\n 'all stages not found' => [\n 'currentDealStages' => [10, 20],\n 'mockStages' => [], // No stages found\n 'expectedUuids' => [], // Updated to reflect that nulls are filtered out\n ],\n ];\n }\n\n #[DataProvider('getTeamGroupsDataProvider')]\n public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n if ($mockTeamData === null) {\n // Team not found\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn(null);\n } else {\n // Team found - create mock team with groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create mock groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n\n // Create mock Group objects\n $groupObjects = [];\n foreach ($mockGroups as $groupData) {\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getUuid')->willReturn($groupData['id']);\n $mockGroup->method('getName')->willReturn($groupData['name']);\n $groupObjects[] = $mockGroup;\n }\n\n // Mock the groups collection to return our mock groups\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator($groupObjects));\n\n // Mock the groups() relation\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')\n ->with($teamUuid)\n ->willReturn($mockTeam);\n }\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups($teamUuid);\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamGroupsWithNonExistentTeam(): void\n {\n // Create mock TeamRepository that returns null (team not found)\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn(null);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamGroupsWithEmptyGroups(): void\n {\n // Create mock team with no groups\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n\n // Create empty groups collection\n $mockGroupsCollection = $this->createMock(\\Illuminate\\Database\\Eloquent\\Collection::class);\n $mockGroupsCollection->method('getIterator')->willReturn(new \\ArrayIterator([]));\n\n $mockGroupsRelation = $this->createMock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeamGroups('team-with-no-groups');\n\n $this->assertEquals([], $result);\n }\n\n public static function getTeamGroupsDataProvider(): array\n {\n return [\n 'team with single group' => [\n 'teamUuid' => 'team-uuid-123',\n 'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'team with multiple groups' => [\n 'teamUuid' => 'team-uuid-456',\n 'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],\n 'mockGroups' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n 'expectedResult' => [\n ['id' => 'group-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'group-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'group-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'team not found' => [\n 'teamUuid' => 'non-existent-uuid',\n 'mockTeamData' => null,\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n 'team with no groups' => [\n 'teamUuid' => 'team-uuid-empty',\n 'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],\n 'mockGroups' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('getTeamsDataProvider')]\n public function testGetTeams(array $mockTeams, array $expectedResult): void\n {\n // Create mock TeamRepository\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n // Create mock Team objects\n $teamObjects = [];\n foreach ($mockTeams as $teamData) {\n $mockTeam = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam->method('getUuid')->willReturn($teamData['id']);\n $mockTeam->method('getName')->willReturn($teamData['name']);\n $mockTeam->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn($teamData['hasAutomatedReports']);\n $teamObjects[] = $mockTeam;\n }\n\n // Mock the repository to return a Collection (not array)\n $mockTeamRepository->method('getTeamsForKiosk')\n ->with('active')\n ->willReturn(new Collection($teamObjects));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetTeamsWithNoTeams(): void\n {\n // Create mock TeamRepository that returns empty Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsWithAllTeamsWithoutFeature(): void\n {\n // Create mock teams without AUTOMATED_REPORTS feature\n $mockTeam1 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam1->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n $mockTeam2 = $this->createMock(\\Jiminny\\Models\\Team::class);\n $mockTeam2->method('hasFeature')\n ->with(\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(false);\n\n // Create mock TeamRepository that returns Collection\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n // Create service with mocked TeamRepository\n $automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $automatedReportsService->getTeams();\n\n $this->assertEquals([], $result);\n }\n\n public function testGetTeamsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeams(partner: $partner);\n }\n\n public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getOrganizationFieldData(partner: $partner);\n }\n\n public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void\n {\n $partner = $this->createMock(Partner::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects(self::once())\n ->method('getTeamsForKiosk')\n ->with(Team::STATUS_ACTIVE, $partner)\n ->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $service->getTeamsGroupsOptions(partner: $partner);\n }\n\n public static function getTeamsDataProvider(): array\n {\n return [\n 'single team with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ],\n ],\n 'multiple teams with feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-2', 'name' => 'Marketing Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'mixed teams - some with feature, some without' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => true,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-3',\n 'name' => 'Support Team',\n 'hasAutomatedReports' => true,\n ],\n ],\n 'expectedResult' => [\n ['id' => 'team-uuid-1', 'name' => 'Sales Team'],\n ['id' => 'team-uuid-3', 'name' => 'Support Team'],\n ],\n ],\n 'all teams without feature' => [\n 'mockTeams' => [\n [\n 'id' => 'team-uuid-1',\n 'name' => 'Sales Team',\n 'hasAutomatedReports' => false,\n ],\n [\n 'id' => 'team-uuid-2',\n 'name' => 'Marketing Team',\n 'hasAutomatedReports' => false,\n ],\n ],\n 'expectedResult' => [],\n ],\n 'empty teams array' => [\n 'mockTeams' => [],\n 'expectedResult' => [],\n ],\n ];\n }\n\n #[DataProvider('deleteS3FilesDataProvider')]\n public function testDeleteS3Files(\n string $mediaType,\n array $expectedFileExtensions,\n array $existingFiles,\n string $pathSuffix,\n int $expectedDeletes\n ): void {\n // Arrange\n $teamUuid = 'team-uuid-123';\n $reportUuid = 'report-uuid-456';\n $basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);\n\n $team = Mockery::mock(Team::class);\n $team->allows('getUuid')->andReturn($teamUuid);\n\n $report = Mockery::mock(AutomatedReport::class);\n $report->allows('getTeam')->andReturn($team);\n\n $result = Mockery::mock(AutomatedReportResult::class);\n $result->allows('getReport')->andReturn($report);\n $result->allows('getUuid')->andReturn($reportUuid);\n $result->allows('getMediaType')->andReturn($mediaType);\n\n Storage::fake();\n Log::shouldReceive('info')->times($expectedDeletes);\n\n foreach ($existingFiles as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n Storage::put($filePath, 'dummy content');\n }\n\n // Act\n $this->service->deleteS3Files($result);\n\n // Assert\n foreach ($expectedFileExtensions as $extension) {\n $filePath = $basePath . $pathSuffix . '.' . $extension;\n if (in_array($extension, $existingFiles, true)) {\n Storage::assertMissing($filePath);\n } else {\n // To be sure no unexpected files were created and deleted\n Storage::assertMissing($filePath);\n }\n }\n }\n\n public static function deleteS3FilesDataProvider(): array\n {\n return [\n 'PDF report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'MD', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 3,\n ],\n 'PDF report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => ['html', 'pdf'],\n 'pathSuffix' => '',\n 'expectedDeletes' => 2,\n ],\n 'PDF report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,\n 'expectedFileExtensions' => ['html', 'MD', 'pdf'],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n 'Podcast report, all files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['json', 'mp3', 'ssml'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 3,\n ],\n 'Podcast report, some files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => ['mp3'],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 1,\n ],\n 'Podcast report, no files exist' => [\n 'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,\n 'expectedFileExtensions' => ['json', 'mp3', 'ssml'],\n 'existingFiles' => [],\n 'pathSuffix' => '_podcast',\n 'expectedDeletes' => 0,\n ],\n 'Other media type, should do nothing' => [\n 'mediaType' => 'some_other_type',\n 'expectedFileExtensions' => [],\n 'existingFiles' => [],\n 'pathSuffix' => '',\n 'expectedDeletes' => 0,\n ],\n ];\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void\n {\n // Create mocks for the test\n $automatedReportsService = Mockery::mock(AutomatedReportsService::class);\n\n $team = Mockery::mock(Team::class);\n $team->shouldReceive('getId')->andReturn(123);\n\n $from = now()->subDays(30);\n $to = now();\n $source = 'test-source';\n\n // Expect the method to be called with specific parameters\n $automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')\n ->once()\n ->with(\n $team,\n Mockery::on(function ($arg) use ($from) {\n return $arg->timestamp === $from->timestamp;\n }),\n Mockery::on(function ($arg) use ($to) {\n return $arg->timestamp === $to->timestamp;\n }),\n $source\n )\n ->andReturn(5);\n\n // Call the method and verify the result\n $result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(\n $team,\n $from,\n $to,\n $source\n );\n\n $this->assertEquals(5, $result);\n }\n\n #[DataProvider('sanitizeFileNameDataProvider')]\n public function testSanitizeFileName(string $input, string $expected): void\n {\n $result = $this->service->sanitizeFileName($input);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function sanitizeFileNameDataProvider(): array\n {\n return [\n 'no special characters' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team',\n ],\n 'forward slash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND/IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'backslash in team name' => [\n 'input' => 'Exec Summary - Sep 2025 - ND\\IRV',\n 'expected' => 'Exec Summary - Sep 2025 - ND-IRV',\n ],\n 'multiple forward slashes' => [\n 'input' => 'Report - Team A/B/C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'multiple backslashes' => [\n 'input' => 'Report - Team A\\B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'mixed slashes and backslashes' => [\n 'input' => 'Report - Team A/B\\C',\n 'expected' => 'Report - Team A-B-C',\n ],\n 'complex team name with slashes' => [\n 'input' => 'Exec Summary - Sep 2025 - Business Development Team - ND/IRV, Net Driven - Acquisition (Sales)',\n 'expected' => 'Exec Summary - Sep 2025 - Business Development Team - ND-IRV, Net Driven - Acquisition (Sales)',\n ],\n 'only slashes' => [\n 'input' => '//\\\\\\\\',\n 'expected' => '----',\n ],\n 'empty string' => [\n 'input' => '',\n 'expected' => '',\n ],\n 'slash at start' => [\n 'input' => '/Report Name',\n 'expected' => '-Report Name',\n ],\n 'slash at end' => [\n 'input' => 'Report Name/',\n 'expected' => 'Report Name-',\n ],\n ];\n }\n\n public function testGetReportFileNameSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with slash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('ND/IRV, Net Driven - Acquisition (Sales)');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileName\n $result = $service->getReportFileName($mockReportResult);\n\n // Verify the result does not contain slashes or backslashes\n $this->assertStringNotContainsString('/', $result);\n $this->assertStringNotContainsString('\\\\', $result);\n\n // Verify the slash was replaced with dash\n $this->assertStringContainsString('ND-IRV', $result);\n }\n\n public function testGetReportFileNameWithExtensionSanitizesOutput(): void\n {\n // Create mock GroupRepository\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n\n // Create mock Group with backslash in name\n $mockGroup = $this->createMock(\\Jiminny\\Models\\Group::class);\n $mockGroup->method('getName')->willReturn('Team\\Name');\n\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n // Create service with mocked GroupRepository\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n // Create mock AutomatedReportResult\n $mockReportResult = $this->createMock(AutomatedReportResult::class);\n\n // Create mock AutomatedReport\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getFrequency')->willReturn('monthly');\n\n $mockReportResult->method('getReport')->willReturn($mockReport);\n $mockReportResult->method('getFromDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-01')\n );\n $mockReportResult->method('getToDate')->willReturn(\n \\Illuminate\\Support\\Carbon::parse('2025-09-30')\n );\n $mockReportResult->method('getGroups')->willReturn([123]);\n $mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n // Call getReportFileNameWithExtension\n $result = $service->getReportFileNameWithExtension($mockReportResult);\n\n // Verify the result does not contain backslashes\n $this->assertStringNotContainsString('\\\\', $result);\n $this->assertStringNotContainsString('/', $result);\n\n // Verify the backslash was replaced with dash\n $this->assertStringContainsString('Team-Name', $result);\n\n // Verify extension is added\n $this->assertStringEndsWith('.pdf', $result);\n }\n\n public function testHasPassedScheduledTimeWithNullGeneratedAt(): void\n {\n $result = $this->service->hasPassedScheduledTime(null, 'America/Chicago');\n\n $this->assertFalse($result);\n }\n\n public function testHasPassedScheduledTimeWhenScheduledTimePassed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testHasPassedScheduledTimeBeforeScheduledTimeToday(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 04:00:00', 'America/Chicago'));\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->hasPassedScheduledTime($generatedAt, 'America/Chicago');\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWithEmptyUsers(): void\n {\n $result = $this->service->shouldSendReport([]);\n\n $this->assertFalse($result);\n }\n\n public function testShouldSendReportAtScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 05:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportNotAtScheduledTimeWithoutGeneratedAt(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $result = $this->service->shouldSendReport($users);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenScheduledTimeMissed(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 01:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertTrue($result);\n\n Carbon::setTestNow();\n }\n\n public function testShouldSendReportWhenGeneratedAfterScheduledTime(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-02-24 10:00:00', 'America/Chicago'));\n\n $users = [\n ['email' => 'test@example.com', 'name' => 'Test User', 'timezone' => 'America/Chicago'],\n ];\n\n $generatedAt = Carbon::parse('2026-02-24 06:00:00', 'America/Chicago');\n\n $result = $this->service->shouldSendReport($users, $generatedAt);\n\n $this->assertFalse($result);\n\n Carbon::setTestNow();\n }\n\n public function testGetTypes(): void\n {\n $types = AutomatedReportsService::getTypes();\n\n $this->assertIsArray($types);\n $this->assertContains('exec_summary', $types);\n $this->assertContains('coaching_profiles', $types);\n $this->assertContains('loss_analysis', $types);\n $this->assertNotContains('ask_jiminny', $types);\n }\n\n public function testGetCallTypes(): void\n {\n $callTypes = AutomatedReportsService::getCallTypes();\n\n $this->assertIsArray($callTypes);\n $this->assertContains('conference', $callTypes);\n $this->assertContains('dialer', $callTypes);\n }\n\n public function testGetFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertContains('quarterly', $frequencies);\n $this->assertContains('one_off', $frequencies);\n $this->assertNotContains('daily', $frequencies);\n }\n\n public function testGetAskJiminnyFrequencies(): void\n {\n $frequencies = AutomatedReportsService::getAskJiminnyFrequencies();\n\n $this->assertIsArray($frequencies);\n $this->assertContains('daily', $frequencies);\n $this->assertContains('weekly', $frequencies);\n $this->assertContains('monthly', $frequencies);\n $this->assertNotContains('quarterly', $frequencies);\n $this->assertNotContains('one_off', $frequencies);\n }\n\n public function testGetReportEnabledFieldData(): void\n {\n $result = $this->service->getReportEnabledFieldData(true);\n\n $this->assertEquals('report_enabled', $result['id']);\n $this->assertTrue($result['value']);\n }\n\n public function testGetReportEnabledFieldDataDefault(): void\n {\n $result = $this->service->getReportEnabledFieldData();\n\n $this->assertFalse($result['value']);\n }\n\n public function testGetOrganizationFieldDataShortVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData(null, true);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetOrganizationFieldDataFullVersion(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getOrganizationFieldData('team-uuid-1', false);\n\n $this->assertEquals('organization', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('team-uuid-1', $result['value']);\n $this->assertArrayHasKey('dependencies', $result);\n }\n\n public function testGetTeamFieldDataShortVersion(): void\n {\n $result = $this->service->getTeamFieldData([], [], true);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetTeamFieldDataFullVersion(): void\n {\n $result = $this->service->getTeamFieldData(['opt1'], ['val1'], false);\n\n $this->assertEquals('teams', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals(['opt1'], $result['options']);\n $this->assertEquals(['val1'], $result['value']);\n }\n\n public function testGetReportTypeFieldDataShortVersion(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayNotHasKey('inputType', $result);\n }\n\n public function testGetReportTypeFieldDataFullVersion(): void\n {\n $result = $this->service->getReportTypeFieldData('exec_summary', false);\n\n $this->assertEquals('report_type', $result['id']);\n $this->assertArrayHasKey('inputType', $result);\n $this->assertEquals('exec_summary', $result['value']);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingBothFeatures(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertLessThan(\n array_search(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids),\n array_search('exec_summary', $ids)\n );\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAutomatedReports(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, true],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, false],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetReportTypeFieldDataWithTeamHavingOnlyAskJiminny(): void\n {\n $team = $this->createMock(\\Jiminny\\Models\\Team::class);\n $team->method('hasFeature')->willReturnMap([\n [\\Jiminny\\Models\\Feature\\FeatureEnum::AUTOMATED_REPORTS, false],\n [\\Jiminny\\Models\\Feature\\FeatureEnum::ASK_JIMINNY_REPORTS, true],\n ]);\n\n $result = $this->service->getReportTypeFieldData(null, true, $team);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n $this->assertNotContains('exec_summary', $ids);\n }\n\n public function testGetReportTypeFieldDataWithNullTeamFallsBackToStandardTypes(): void\n {\n $result = $this->service->getReportTypeFieldData(null, true, null);\n\n $ids = array_column($result['options'], 'id');\n $this->assertContains('exec_summary', $ids);\n $this->assertNotContains(AutomatedReportsService::TYPE_ASK_JIMINNY, $ids);\n }\n\n public function testGetFrequencyFieldData(): void\n {\n $result = $this->service->getFrequencyFieldData('weekly');\n\n $this->assertEquals('frequency', $result['id']);\n $this->assertEquals('weekly', $result['value']);\n $this->assertArrayHasKey('options', $result);\n }\n\n public function testGetPeriodFieldData(): void\n {\n $result = $this->service->getPeriodFieldData('2025-01-01', '2025-01-31');\n\n $this->assertEquals('period', $result['id']);\n $this->assertEquals('2025-01-01', $result['value']['startDate']);\n $this->assertEquals('2025-01-31', $result['value']['endDate']);\n }\n\n public function testGetCallDurationFieldData(): void\n {\n $result = $this->service->getCallDurationFieldData(5, 60);\n\n $this->assertEquals('call_duration', $result['id']);\n $this->assertEquals(5, $result['value']['min']);\n $this->assertEquals(60, $result['value']['max']);\n }\n\n public function testGetDealValueFieldData(): void\n {\n $result = $this->service->getDealValueFieldData(1000, 5000);\n\n $this->assertEquals('deal_value', $result['id']);\n $this->assertEquals(1000, $result['value']['min']);\n $this->assertEquals(5000, $result['value']['max']);\n }\n\n public function testGetCustomReportNameFieldData(): void\n {\n $result = $this->service->getCustomReportNameFieldData('My Report');\n\n $this->assertEquals('custom_name', $result['id']);\n $this->assertEquals('My Report', $result['value']);\n }\n\n public function testGetAdditionalPromptInputFieldData(): void\n {\n $result = $this->service->getAdditionalPromptInputFieldData('Some prompt');\n\n $this->assertEquals('additional_prompt_input', $result['id']);\n $this->assertEquals('Some prompt', $result['value']);\n }\n\n public function testGetCallTypeFieldDataBothOn(): void\n {\n $result = $this->service->getCallTypeFieldData(true, true);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertCount(2, $result['value']);\n }\n\n public function testGetCallTypeFieldDataNoneOn(): void\n {\n $result = $this->service->getCallTypeFieldData(false, false);\n\n $this->assertEquals('call_type', $result['id']);\n $this->assertEmpty($result['value']);\n }\n\n public function testGetCallTypeFieldDataConferenceOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(true, false);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('conference', $result['value'][0]['id']);\n }\n\n public function testGetCallTypeFieldDataDialerOnly(): void\n {\n $result = $this->service->getCallTypeFieldData(false, true);\n\n $this->assertCount(1, $result['value']);\n $this->assertEquals('dialer', $result['value'][0]['id']);\n }\n\n public function testTransformDurationToMinutesNull(): void\n {\n $result = $this->service->transformDurationToMinutes(null);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutesZero(): void\n {\n $result = $this->service->transformDurationToMinutes(0);\n\n $this->assertNull($result);\n }\n\n public function testTransformDurationToMinutes(): void\n {\n $result = $this->service->transformDurationToMinutes(300);\n\n $this->assertEquals(5, $result);\n }\n\n public function testGetTeam(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('idOrUuid')\n ->with('team-uuid')\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeam('team-uuid');\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetTeamById(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n $mockTeamRepository->expects($this->once())\n ->method('find')\n ->with(42)\n ->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n $result = $service->getTeamById(42);\n\n $this->assertSame($mockTeam, $result);\n }\n\n public function testGetGroupsUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([]);\n\n $result = $this->service->getGroupsUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetGroupsUuidsWithGroups(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getUuid')->willReturn('group-uuid-1');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getGroups')->willReturn([10, 99]);\n\n $result = $service->getGroupsUuids($report);\n\n $this->assertEquals(['group-uuid-1'], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([]);\n\n $result = $this->service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetPlaybookCategoriesUuidsWithCategories(): void\n {\n $mockCategory = $this->createMock(\\Jiminny\\Models\\PlaybookCategory::class);\n $mockCategory->method('getUuid')->willReturn('cat-uuid-1');\n\n $mockPlaybookCategoryRepository = $this->createMock(PlaybookCategoryRepository::class);\n $mockPlaybookCategoryRepository->method('find')->willReturnMap([\n [1, $mockCategory],\n [2, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $mockPlaybookCategoryRepository,\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getPlaybookCategories')->willReturn([1, 2]);\n\n $result = $service->getPlaybookCategoriesUuids($report);\n\n $this->assertEquals(['cat-uuid-1'], $result);\n }\n\n public function testGetDealAtCallStagesUuidsEmpty(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([]);\n\n $result = $this->service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetDealAtCallStagesUuidsWithStages(): void\n {\n $mockStage = $this->createMock(\\Jiminny\\Models\\Stage::class);\n $mockStage->method('getUuid')->willReturn('stage-uuid-1');\n\n $mockStageRepository = $this->createMock(StageRepository::class);\n $mockStageRepository->method('find')->willReturnMap([\n [5, $mockStage],\n [9, null],\n ]);\n\n $service = $this->getService(mockStageRepository: $mockStageRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getDealAtCallStages')->willReturn([5, 9]);\n\n $result = $service->getDealAtCallStagesUuids($report);\n\n $this->assertEquals(['stage-uuid-1'], $result);\n }\n\n public function testGetJiminnyUsersUuids(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getJiminnyUsersUuids($report);\n\n $this->assertEquals(['user-uuid-1'], $result);\n }\n\n public function testGetRecipientUsers(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getEmailAddress')->willReturn('user@test.com');\n $mockUser->method('getName')->willReturn('Test User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n\n $result = $service->getRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n $this->assertEquals('Test User', $result[0]['name']);\n }\n\n public function testGetValidRecipientUsersFiltersEmptyEmail(): void\n {\n $mockUserWithEmail = $this->createMock(User::class);\n $mockUserWithEmail->method('getEmailAddress')->willReturn('valid@test.com');\n $mockUserWithEmail->method('getName')->willReturn('Valid User');\n $timezone = $this->createMock(\\DateTimeZone::class);\n $timezone->method('getName')->willReturn('UTC');\n $mockUserWithEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserNoEmail = $this->createMock(User::class);\n $mockUserNoEmail->method('getEmailAddress')->willReturn('');\n $mockUserNoEmail->method('getName')->willReturn('No Email User');\n $mockUserNoEmail->method('getTimezone')->willReturn($timezone);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUserWithEmail],\n [2, $mockUserNoEmail],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1, 2]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('valid@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersWithJiminny(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $mockUser1 = $this->createMock(User::class);\n $mockUser1->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser1->method('getName')->willReturn('User1');\n $mockUser1->method('getTimezone')->willReturn($tz);\n\n $mockUser2 = $this->createMock(User::class);\n $mockUser2->method('getEmailAddress')->willReturn('jiminny@test.com');\n $mockUser2->method('getName')->willReturn('Jiminny');\n $mockUser2->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, $mockUser1],\n [2, $mockUser2],\n ]);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => [2]]);\n\n $result = $service->getValidRecipientUsers($report, true);\n\n $this->assertCount(2, $result);\n }\n\n public function testGetReportTypeName(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getType')->willReturn('exec_summary');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n\n $result = $this->service->getReportTypeName($mockResult);\n\n $this->assertEquals('Exec Summary', $result);\n }\n\n public function testGetReportPeriodNameThrowsOnNullFrom(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2025-01-15'));\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n public function testGetReportPeriodNameThrowsOnNullTo(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2025-01-08'));\n $mockResult->method('getToDate')->willReturn(null);\n\n $this->expectException(\\Jiminny\\Exceptions\\ApplicationException::class);\n\n $this->service->getReportPeriodName($mockResult);\n }\n\n #[DataProvider('formatReportPeriodNameDataProvider')]\n public function testGetReportPeriodName(\n string $frequency,\n string $from,\n string $to,\n string $expected\n ): void {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn($frequency);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse($from));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse($to));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals($expected, $result);\n }\n\n public static function formatReportPeriodNameDataProvider(): array\n {\n return [\n 'daily' => [\n 'frequency' => 'daily',\n 'from' => '2025-05-15',\n 'to' => '2025-05-15',\n 'expected' => '15 May 2025',\n ],\n 'monthly same year' => [\n 'frequency' => 'monthly',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => 'May 2025',\n ],\n 'weekly same month' => [\n 'frequency' => 'weekly',\n 'from' => '2025-08-04',\n 'to' => '2025-08-08',\n 'expected' => '4 - 8 Aug 2025',\n ],\n 'weekly different months same year' => [\n 'frequency' => 'weekly',\n 'from' => '2025-10-27',\n 'to' => '2025-11-03',\n 'expected' => '27 Oct - 3 Nov 2025',\n ],\n 'weekly different years' => [\n 'frequency' => 'weekly',\n 'from' => '2024-12-28',\n 'to' => '2025-01-03',\n 'expected' => '28 Dec 2024 - 3 Jan 2025',\n ],\n 'quarterly same year' => [\n 'frequency' => 'quarterly',\n 'from' => '2025-01-01',\n 'to' => '2025-04-01',\n 'expected' => 'Jan - Mar 2025',\n ],\n 'quarterly different years' => [\n 'frequency' => 'quarterly',\n 'from' => '2024-11-01',\n 'to' => '2025-02-01',\n 'expected' => 'Nov 2024 - Jan 2025',\n ],\n 'one_off same month' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-02',\n 'to' => '2025-05-31',\n 'expected' => '2 - 31 May 2025',\n ],\n 'one_off different months same year' => [\n 'frequency' => 'one_off',\n 'from' => '2025-05-15',\n 'to' => '2025-06-15',\n 'expected' => '15 May - 15 Jun 2025',\n ],\n 'one_off different years' => [\n 'frequency' => 'one_off',\n 'from' => '2024-12-15',\n 'to' => '2025-01-15',\n 'expected' => '15 Dec 2024 - 15 Jan 2025',\n ],\n 'unknown frequency falls back to default' => [\n 'frequency' => 'unknown',\n 'from' => '2025-05-01',\n 'to' => '2025-05-31',\n 'expected' => '1 May 2025 - 31 May 2025',\n ],\n ];\n }\n\n public function testGetReportTeamsNameEmpty(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([]);\n\n $result = $this->service->getReportTeamsName($mockResult);\n\n $this->assertEquals('All', $result);\n }\n\n public function testGetReportTeamsNameSingleGroup(): void\n {\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getName')->willReturn('Sales Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team', $result);\n }\n\n public function testGetReportTeamsNameMultipleGroups(): void\n {\n $mockGroup1 = $this->createMock(Group::class);\n $mockGroup1->method('getName')->willReturn('Sales Team');\n\n $mockGroup2 = $this->createMock(Group::class);\n $mockGroup2->method('getName')->willReturn('Marketing Team');\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturnMap([\n [10, $mockGroup1],\n [20, $mockGroup2],\n [99, null],\n ]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getGroups')->willReturn([10, 20, 99]);\n\n $result = $service->getReportTeamsName($mockResult);\n\n $this->assertEquals('Sales Team, Marketing Team', $result);\n }\n\n public function testGetReportFound(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReport('report-uuid');\n\n $this->assertSame($mockReport, $result);\n }\n\n public function testGetReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReport('non-existent-uuid');\n }\n\n public function testDeleteReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->delete('non-existent-uuid');\n }\n\n public function testDeleteReportSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('delete');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $service->delete('report-uuid');\n }\n\n public function testUpdateStatusNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->updateStatus('non-existent-uuid', ['report_enabled' => true]);\n }\n\n public function testGetReportResultFound(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findResultByUuid')\n ->with('result-uuid')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResult('result-uuid');\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testGetReportResultNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findResultByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->getReportResult('non-existent-uuid');\n }\n\n public function testFindChildResult(): void\n {\n $mockParent = $this->createMock(AutomatedReportResult::class);\n $mockChild = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findChildResult')\n ->with($mockParent, 'podcast')\n ->willReturn($mockChild);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->findChildResult($mockParent, 'podcast');\n\n $this->assertSame($mockChild, $result);\n }\n\n #[DataProvider('calculateFromAndToDatePeriodDataProvider')]\n public function testCalculateFromAndToDatePeriod(string $frequency): void\n {\n Carbon::setTestNow(Carbon::parse('2025-06-15 12:00:00'));\n\n $result = $this->service->calculateFromAndToDatePeriod($frequency);\n\n $this->assertArrayHasKey('fromDate', $result);\n $this->assertArrayHasKey('toDate', $result);\n $this->assertInstanceOf(Carbon::class, $result['fromDate']);\n $this->assertInstanceOf(Carbon::class, $result['toDate']);\n\n Carbon::setTestNow();\n }\n\n public static function calculateFromAndToDatePeriodDataProvider(): array\n {\n return [\n 'daily' => ['daily'],\n 'weekly' => ['weekly'],\n 'monthly' => ['monthly'],\n 'quarterly' => ['quarterly'],\n ];\n }\n\n public function testCalculateFromAndToDatePeriodOneOff(): void\n {\n $from = IlluminateCarbon::parse('2025-01-01');\n $to = IlluminateCarbon::parse('2025-01-31');\n\n $result = $this->service->calculateFromAndToDatePeriod('one_off', $from, $to);\n\n $this->assertSame($from, $result['fromDate']);\n $this->assertSame($to, $result['toDate']);\n }\n\n public function testCalculateFromAndToDatePeriodInvalidFrequency(): void\n {\n $this->expectException(InvalidArgumentException::class);\n\n $this->service->calculateFromAndToDatePeriod('invalid_frequency');\n }\n\n public function testGetMediaPath(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn('https://example.com/reports/file.pdf');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/reports/file.pdf', $result);\n }\n\n public function testGetMediaPathPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n $mockResult->method('getPodcastAudioUrl')->willReturn('https://example.com/audio/file.mp3');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertEquals('/audio/file.mp3', $result);\n }\n\n public function testGetMediaPathNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMediaPathPdfNullUrl(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->method('getPdfUrl')->willReturn(null);\n\n $result = $this->service->getMediaPath($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetFilenameSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertEquals('Podcast', $result);\n }\n\n public function testGetFilenameSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getFilenameSuffix($mockResult);\n\n $this->assertNull($result);\n }\n\n public function testGetMailSubjectSuffixPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('report', $result);\n }\n\n public function testGetMailSubjectSuffixPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('podcast', $result);\n }\n\n public function testGetMailSubjectSuffixUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown_type');\n\n $result = $this->service->getMailSubjectSuffix($mockResult);\n\n $this->assertEquals('', $result);\n }\n\n public function testGetMediaTypeMetadataPdf(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('pdf', $result['extension']);\n $this->assertEquals('application/pdf', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataPodcast(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertEquals('mp3', $result['extension']);\n $this->assertEquals('audio/mpeg', $result['mime']);\n }\n\n public function testGetMediaTypeMetadataUnknown(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getMediaType')->willReturn('unknown');\n\n $result = $this->service->getMediaTypeMetadata($mockResult);\n\n $this->assertNull($result['extension']);\n $this->assertNull($result['mime']);\n }\n\n public function testGetTeamIdsWithReportsResults(): void\n {\n $expected = new Collection([1, 2, 3]);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getTeamIdsWithReportsResults')\n ->with(5)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamIdsWithReportsResults(5);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetTeamReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getTeamReports($mockTeam);\n\n $this->assertSame($expected, $result);\n }\n\n public function testGetReportResults(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $expected = new \\Illuminate\\Database\\Eloquent\\Collection();\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn($expected);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getReportResults($mockReport);\n\n $this->assertSame($expected, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodNoReports(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportIdsByTeam')\n ->willReturn(new Collection([]));\n $mockRepo->expects($this->never())\n ->method('getReportResultsQueryForRetention');\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testDeleteReportsResultsInRetentionPeriodWithNoQueryResults(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n $retentionDate = \\Carbon\\CarbonImmutable::parse('2025-01-01');\n\n $mockQuery = Mockery::mock(Builder::class);\n $mockQuery->shouldReceive('exists')->andReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('getReportIdsByTeam')->willReturn(new Collection([1, 2]));\n $mockRepo->method('getReportResultsQueryForRetention')->willReturn($mockQuery);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $result = $service->deleteReportsResultsInRetentionPeriod($mockTeam, $retentionDate);\n\n $this->assertEquals(0, $result);\n }\n\n public function testUpdateAskJiminnyReportStatusNotAskJiminny(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())->method('isAskJiminnyReport')->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $mockUser = $this->createMock(User::class);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report is not an Ask Jiminny report');\n\n $service->updateAskJiminnyReport($mockReport, [], $mockUser);\n }\n\n public function testUpdateAskJiminnyReportStatusThrowsWhenEnablingWithMissingReferences(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(false);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->never())->method('update');\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('This report is missing a saved search or prompt.');\n\n $service->updateAskJiminnyReportStatus($mockReport, true);\n }\n\n public function testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->never())->method('isAskJiminnyReport');\n $mockReport->expects($this->never())->method('canExecute');\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => false])\n ->willThrowException(new \\RuntimeException('stop-after-update'));\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('stop-after-update');\n\n $service->updateAskJiminnyReportStatus($mockReport, false);\n }\n\n public function testUpdateAskJiminnyReportStatusSuccess(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->expects($this->once())\n ->method('isAskJiminnyReport')\n ->willReturn(true);\n $mockReport->expects($this->once())\n ->method('canExecute')\n ->willReturn(true);\n\n $mockFreshReport = $this->createMock(AutomatedReport::class);\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('update')\n ->with($mockReport, ['status' => true])\n ->willReturn($mockReport);\n $mockReport->expects($this->once())\n ->method('fresh')\n ->willReturn($mockFreshReport);\n\n $service = $this->getService(mockRepo: $mockRepo);\n\n $result = $service->updateAskJiminnyReportStatus($mockReport, true);\n\n $this->assertSame($mockFreshReport, $result);\n }\n\n public function testGetAskJiminnyReportFilters(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearch->method('getUuid')->willReturn('search-uuid-1');\n $mockSearch->method('getName')->willReturn('My Search');\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->expects($this->once())\n ->method('findByUserOrderedByName')\n ->with($mockUser)\n ->willReturn(new Collection([$mockSearch]));\n\n $mockPromptDto = new AskAnythingPromptDto(\n id: 'prompt-uuid-1',\n title: 'My Prompt',\n content: 'Prompt text',\n target: AskAnythingPromptTarget::on_demand,\n );\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->expects($this->once())\n ->method('get')\n ->with($mockUser, AskAnythingPromptTarget::on_demand)\n ->willReturn([$mockPromptDto]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFilters($mockUser);\n\n $this->assertCount(2, $result);\n $promptFilter = collect($result)->firstWhere('id', 'prompt');\n $searchFilter = collect($result)->firstWhere('id', 'saved_search');\n\n $this->assertCount(1, $promptFilter['options']);\n $this->assertEquals('prompt-uuid-1', $promptFilter['options'][0]['id']);\n $this->assertCount(1, $searchFilter['options']);\n $this->assertEquals('search-uuid-1', $searchFilter['options'][0]['id']);\n }\n\n public function testGetAskJiminnyReportFormDataWithoutReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser);\n\n $this->assertArrayHasKey('fields', $result);\n $this->assertIsArray($result['fields']);\n\n $fieldIds = array_column($result['fields'], 'id');\n $this->assertContains('enabled', $fieldIds);\n $this->assertContains('report_name', $fieldIds);\n $this->assertContains('frequency', $fieldIds);\n $this->assertContains('expires_on', $fieldIds);\n $this->assertContains('saved_search', $fieldIds);\n $this->assertContains('ask_jiminny_prompt', $fieldIds);\n }\n\n public function testGetAskJiminnyReportFormDataWithReport(): void\n {\n $timezone = new \\DateTimeZone('UTC');\n\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getTimezone')->willReturn($timezone);\n\n $mockTeam = $this->createMock(Team::class);\n $mockUser->method('getTeam')->willReturn($mockTeam);\n\n $mockSavedSearch = $this->createMock(Search::class);\n $mockSavedSearch->method('getUuid')->willReturn('search-uuid');\n $mockSavedSearch->method('getName')->willReturn('My Search');\n\n $mockPromptModel = $this->createMock(AskAnythingPrompt::class);\n $mockPromptModel->method('getUuid')->willReturn('prompt-uuid');\n $mockPromptModel->method('getTitle')->willReturn('My Prompt');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getCustomName')->willReturn('Test Report');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getExpiresAt')->willReturn(IlluminateCarbon::parse('2025-12-31'));\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn(['users' => []]);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(1);\n $mockReport->method('getSavedSearch')->willReturn($mockSavedSearch);\n $mockReport->method('getAskAnythingPrompt')->willReturn($mockPromptModel);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUserOrderedByName')->willReturn(new Collection([]));\n\n $mockPromptService = $this->createMock(AskAnythingPromptService::class);\n $mockPromptService->method('get')->willReturn([]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('getAllByTeam')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->method('getRecipientsFieldData')->willReturn(['options' => []]);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $mockRecipientsService,\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $mockPromptService,\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->getAskJiminnyReportFormData($mockUser, $mockReport);\n\n $fields = collect($result['fields'])->keyBy('id');\n\n $this->assertTrue($fields['enabled']['value']);\n $this->assertEquals('Test Report', $fields['report_name']['value']);\n }\n\n public function testValidateAskJiminnyReportDataMissingName(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name is required');\n\n $service->createAskJiminnyReport(['report_name' => ''], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataNameTooLong(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Report name must be 50 characters or less');\n\n $service->createAskJiminnyReport(['report_name' => str_repeat('a', 51)], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataInvalidFrequency(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Frequency must be daily, weekly, or monthly');\n\n $service->createAskJiminnyReport(['report_name' => 'Valid Name', 'frequency' => 'quarterly'], $mockUser);\n }\n\n public function testValidateAskJiminnyReportDataMissingExpiresOn(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresInPast(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be in the past');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2020-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresTooFar(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Expiration date cannot be more than 1 year from now');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => '2099-01-01'],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataExpiresExactlyOneYearLaterTimeOfDayIsAccepted(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-20 09:00:00'));\n\n try {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getId')->willReturn(1);\n $mockUser->method('getTeamId')->willReturn(1);\n\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(10);\n\n $prompt = $this->createMock(AskAnythingPrompt::class);\n $prompt->method('getId')->willReturn(5);\n\n $activitySearchRepository = $this->createMock(SearchRepository::class);\n $activitySearchRepository->method('findByUuidAndUser')->willReturn($savedSearch);\n\n $askAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $askAnythingRepository->method('getPromptByUuid')->willReturn($prompt);\n\n $automatedReportsRepository = $this->createMock(AutomatedReportsRepository::class);\n $automatedReportsRepository->method('create')->willReturn($this->createMock(AutomatedReport::class));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $automatedReportsRepository,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $activitySearchRepository,\n $askAnythingRepository,\n );\n\n $service->createAskJiminnyReport([\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => '2027-04-20T23:00:00',\n 'saved_search' => 'some-uuid',\n 'ask_jiminny_prompt' => 'prompt-uuid',\n ], $mockUser);\n\n $this->assertTrue(true);\n } finally {\n Carbon::setTestNow();\n }\n }\n\n public function testValidateAskJiminnyReportDataMissingSavedSearch(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search is required');\n\n $service->createAskJiminnyReport(\n ['report_name' => 'Valid Name', 'frequency' => 'daily', 'expires_on' => now()->addMonth()->toDateString()],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Saved search not found or does not belong to you');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'non-existent-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataMissingPrompt(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt is required');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n ],\n $mockUser\n );\n }\n\n public function testValidateAskJiminnyReportDataPromptNotFound(): void\n {\n $mockUser = $this->createMock(User::class);\n\n $mockSearch = $this->createMock(Search::class);\n $mockSearchRepository = $this->createMock(SearchRepository::class);\n $mockSearchRepository->method('findByUuidAndUser')->willReturn($mockSearch);\n\n $mockAskAnythingRepository = $this->createMock(AskAnythingRepository::class);\n $mockAskAnythingRepository->method('getPromptByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $mockSearchRepository,\n $mockAskAnythingRepository,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Ask Jiminny prompt not found');\n\n $service->createAskJiminnyReport(\n [\n 'report_name' => 'Valid Name',\n 'frequency' => 'daily',\n 'expires_on' => now()->addMonth()->toDateString(),\n 'saved_search' => 'search-uuid',\n 'ask_jiminny_prompt' => 'non-existent-prompt-uuid',\n ],\n $mockUser\n );\n }\n\n public function testTransformRecipientsWithNullUsers(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($this->service, []);\n\n $this->assertEquals([], $result);\n }\n\n public function testTransformRecipientsWithUsersKey(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockUser->method('getUuid')->willReturn('user-uuid-1');\n $mockUser->method('getName')->willReturn('User One');\n $mockUser->method('getEmailAddress')->willReturn('user1@test.com');\n $mockUser->method('getPhotoUrl')->willReturn(null);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($mockUser);\n\n $service = $this->getService(mockUserRepository: $mockUserRepository);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $method = $reflection->getMethod('transformRecipients');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, ['users' => [1]]);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user-uuid-1', $result[0]['id']);\n }\n\n public function testGetTeamsGroupsOptions(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam->method('getName')->willReturn('Sales Team');\n $mockTeam->method('hasFeature')\n ->with(FeatureEnum::AUTOMATED_REPORTS)\n ->willReturn(true);\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam]));\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions();\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n $this->assertArrayHasKey('groups', $result[0]);\n }\n\n public function testGetTeamsGroupsOptionsWithFilter(): void\n {\n $mockTeamRepository = $this->createMock(TeamRepository::class);\n\n $mockTeam1 = $this->createMock(Team::class);\n $mockTeam1->method('getUuid')->willReturn('team-uuid-1');\n $mockTeam1->method('getName')->willReturn('Sales Team');\n $mockTeam1->method('hasFeature')->willReturn(true);\n\n $mockTeam2 = $this->createMock(Team::class);\n $mockTeam2->method('getUuid')->willReturn('team-uuid-2');\n $mockTeam2->method('getName')->willReturn('Marketing Team');\n $mockTeam2->method('hasFeature')->willReturn(true);\n\n $mockTeamRepository->method('getTeamsForKiosk')\n ->willReturn(new Collection([$mockTeam1, $mockTeam2]));\n\n $mockGroupsRelation = $this->createMock(HasMany::class);\n $mockGroupsRelation->method('get')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([]));\n $mockTeam1->method('groups')->willReturn($mockGroupsRelation);\n\n $mockTeamRepository->method('idOrUuid')->willReturn($mockTeam1);\n\n $service = $this->getService(mockTeamRepository: $mockTeamRepository);\n\n $result = $service->getTeamsGroupsOptions(['team-uuid-1']);\n\n $this->assertCount(1, $result);\n $this->assertEquals('Sales Team', $result[0]['label']);\n }\n\n public function testGetReturnsTransformedReport(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('report-uuid')\n ->willReturn($mockReport);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->get('report-uuid');\n\n $this->assertIsArray($result);\n $this->assertEquals('report-uuid', $result['id']);\n }\n\n public function testGetThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->get('missing-uuid');\n }\n\n public function testListReturnsData(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('exec_summary');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('weekly');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getFrom')->willReturn(null);\n $mockReport->method('getTo')->willReturn(null);\n $mockReport->method('getDealValueMin')->willReturn(null);\n $mockReport->method('getDealValueMax')->willReturn(null);\n $mockReport->method('getCallTypes')->willReturn([]);\n $mockReport->method('getMediaTypes')->willReturn([]);\n $mockReport->method('getCallDurationMin')->willReturn(null);\n $mockReport->method('getCallDurationMax')->willReturn(null);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getDealAtCallStages')->willReturn([]);\n $mockReport->method('getCurrentDealStages')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCreator')->willReturn(null);\n $mockReport->method('getAdditionalPromptInput')->willReturn(null);\n $mockReport->method('getCustomName')->willReturn('My Report');\n $mockReport->method('getCreatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getUpdatedAt')->willReturn(IlluminateCarbon::parse('2025-01-01'));\n $mockReport->method('getDeletedAt')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAllStandardReports')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->list();\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testListAskJiminnyReportsReturnsData(): void\n {\n $mockUser = $this->createMock(User::class);\n $mockTeam = $this->createMock(Team::class);\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getType')->willReturn('ask_jiminny');\n $mockReport->method('getUuid')->willReturn('report-uuid');\n $mockReport->method('getFrequency')->willReturn('daily');\n $mockReport->method('getTeam')->willReturn($mockTeam);\n $mockReport->method('getStatus')->willReturn(true);\n $mockReport->method('getGroups')->willReturn([]);\n $mockReport->method('getRecipients')->willReturn([]);\n $mockReport->method('getCustomName')->willReturn('AJ Report');\n $mockReport->method('getExpiresAt')->willReturn(null);\n $mockReport->method('getSavedSearch')->willReturn(null);\n $mockReport->method('getAskAnythingPrompt')->willReturn(null);\n $mockReport->method('getAttribute')->with('created_by')->willReturn(null);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getAskJiminnyReportsByUser')\n ->with($mockUser, 'created_at', 'desc')\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->listAskJiminnyReports($mockUser);\n\n $this->assertArrayHasKey('data', $result);\n $this->assertCount(1, $result['data']);\n }\n\n public function testGetActivityTypesFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockActivityTypeService = $this->createMock(ActivityTypeService::class);\n $mockActivityTypeService->expects($this->once())\n ->method('getActivityTypeFieldData')\n ->with(team: $mockTeam, value: ['a'], groupIds: ['g1'])\n ->willReturn(['id' => 'activity_types', 'options' => []]);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('activityTypeService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockActivityTypeService);\n\n $result = $service->getActivityTypesFieldData($mockTeam, ['a'], ['g1']);\n\n $this->assertEquals(['id' => 'activity_types', 'options' => []], $result);\n }\n\n public function testGetDealStageAtCallFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getDealStageAtCallFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'deal_stage_at_call']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getDealStageAtCallFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'deal_stage_at_call'], $result);\n }\n\n public function testGetCurrentDealStageFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockDealStagesService = $this->createMock(DealStagesService::class);\n $mockDealStagesService->expects($this->once())\n ->method('getCurrentDealStageFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'current_deal_stage']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('dealStagesService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockDealStagesService);\n\n $result = $service->getCurrentDealStageFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'current_deal_stage'], $result);\n }\n\n public function testGetRecipientsFieldDataDelegatesToService(): void\n {\n $mockTeam = $this->createMock(Team::class);\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getRecipientsFieldData')\n ->with(team: $mockTeam, value: [])\n ->willReturn(['id' => 'recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getRecipientsFieldData($mockTeam);\n\n $this->assertEquals(['id' => 'recipients'], $result);\n }\n\n public function testGetJiminnyRecipientsFieldDataDelegatesToService(): void\n {\n $mockRecipientsService = $this->createMock(RecipientsService::class);\n $mockRecipientsService->expects($this->once())\n ->method('getJiminnyRecipientsFieldData')\n ->with(['user-1'])\n ->willReturn(['id' => 'jiminny_recipients']);\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n $prop = $reflection->getProperty('recipientsService');\n $prop->setAccessible(true);\n $prop->setValue($service, $mockRecipientsService);\n\n $result = $service->getJiminnyRecipientsFieldData(['user-1']);\n\n $this->assertEquals(['id' => 'jiminny_recipients'], $result);\n }\n\n public function testCreateReportResultDelegatesToRepository(): void\n {\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(42);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('createResult')\n ->willReturn($mockResult);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $result = $service->createReportResult($mockReport);\n\n $this->assertSame($mockResult, $result);\n }\n\n public function testDeleteReportResultDeletesS3AndModel(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $service = $reflection->newInstanceWithoutConstructor();\n\n foreach ([\n 'teamRepository' => TeamRepository::class,\n 'groupRepository' => GroupRepository::class,\n 'userRepository' => UserRepository::class,\n 'stageRepository' => StageRepository::class,\n 'dealStagesService' => DealStagesService::class,\n 'recipientsService' => RecipientsService::class,\n 'automatedReportsRepository' => AutomatedReportsRepository::class,\n 'webhookService' => Webhook::class,\n 'dispatcher' => Dispatcher::class,\n 'activityTypeService' => ActivityTypeService::class,\n 'playbookCategoryRepository' => PlaybookCategoryRepository::class,\n 'askAnythingPromptService' => AskAnythingPromptService::class,\n 'activitySearchRepository' => SearchRepository::class,\n 'askAnythingRepository' => AskAnythingRepository::class,\n ] as $propName => $class) {\n $prop = $reflection->getProperty($propName);\n $prop->setAccessible(true);\n $prop->setValue($service, $this->createMock($class));\n }\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteReportResult($mockResult);\n }\n\n public function testDeleteAllReportResultsIteratesAndDeletes(): void\n {\n $mockResult1 = $this->createMock(AutomatedReportResult::class);\n $mockResult1->method('getId')->willReturn(1);\n $mockResult1->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult1->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult1->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult1]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllReportResults($mockReport);\n }\n\n public function testDeleteAllDataDeletesReportsAndResults(): void\n {\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getId')->willReturn(1);\n $mockResult->method('getReport')->willReturn($this->createMock(AutomatedReport::class));\n $mockResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PDF);\n $mockResult->expects($this->once())->method('delete');\n\n $mockReport = $this->createMock(AutomatedReport::class);\n $mockReport->method('getId')->willReturn(10);\n $mockReport->expects($this->once())->method('delete');\n\n $mockTeam = $this->createMock(Team::class);\n $mockTeam->method('getId')->willReturn(1);\n\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->expects($this->once())\n ->method('getReportsByTeam')\n ->with($mockTeam)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockReport]));\n $mockRepo->expects($this->once())\n ->method('getResultsByReport')\n ->with($mockReport)\n ->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$mockResult]));\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n Storage::shouldReceive('exists')->andReturn(false);\n Log::shouldReceive('info')->zeroOrMoreTimes();\n\n $service->deleteAllData($mockTeam);\n }\n\n public function testDeleteReportResultsThrowsWhenReportNotFound(): void\n {\n $mockRepo = $this->createMock(AutomatedReportsRepository::class);\n $mockRepo->method('findByUuid')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $this->createMock(UserRepository::class),\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $mockRepo,\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $this->expectException(ModelNotFoundException::class);\n\n $service->deleteReportResults('missing-uuid');\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('creator@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyDeduplicatesCreatorAndExplicitRecipient(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('shared@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => [1]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersAskJiminnyIncludesGroupMembers(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $creator = $this->createMock(User::class);\n $creator->method('getEmailAddress')->willReturn('creator@test.com');\n $creator->method('getName')->willReturn('Creator');\n $creator->method('getTimezone')->willReturn($tz);\n\n $member = $this->createMock(User::class);\n $member->method('getEmailAddress')->willReturn('member@test.com');\n $member->method('getName')->willReturn('Member');\n $member->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($creator);\n\n $mockGroup = $this->createMock(Group::class);\n $mockGroup->method('getMembers')->willReturn(new \\Illuminate\\Database\\Eloquent\\Collection([$member]));\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn($mockGroup);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([10]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(2, $result);\n $emails = array_column($result, 'email');\n $this->assertContains('creator@test.com', $emails);\n $this->assertContains('member@test.com', $emails);\n }\n\n public function testGetValidRecipientUsersAskJiminnyNullCreatorSkipped(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $shareUser = $this->createMock(User::class);\n $shareUser->method('getEmailAddress')->willReturn('shared@test.com');\n $shareUser->method('getName')->willReturn('Shared');\n $shareUser->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturnMap([\n [1, null],\n [2, $shareUser],\n ]);\n\n $mockGroupRepository = $this->createMock(GroupRepository::class);\n $mockGroupRepository->method('find')->willReturn(null);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $mockGroupRepository,\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn(null);\n $report->method('getRecipients')->willReturn(['users' => [2]]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('shared@test.com', $result[0]['email']);\n }\n\n public function testGetValidRecipientUsersStandardReportDoesNotIncludeCreator(): void\n {\n $tz = $this->createMock(\\DateTimeZone::class);\n $tz->method('getName')->willReturn('UTC');\n\n $user = $this->createMock(User::class);\n $user->method('getEmailAddress')->willReturn('user@test.com');\n $user->method('getName')->willReturn('User');\n $user->method('getTimezone')->willReturn($tz);\n\n $mockUserRepository = $this->createMock(UserRepository::class);\n $mockUserRepository->method('find')->willReturn($user);\n\n $service = new AutomatedReportsService(\n $this->createMock(TeamRepository::class),\n $this->createMock(GroupRepository::class),\n $mockUserRepository,\n $this->createMock(StageRepository::class),\n $this->createMock(DealStagesService::class),\n $this->createMock(RecipientsService::class),\n $this->createMock(AutomatedReportsRepository::class),\n $this->createMock(Webhook::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(ActivityTypeService::class),\n $this->createMock(PlaybookCategoryRepository::class),\n $this->createMock(AskAnythingPromptService::class),\n $this->createMock(SearchRepository::class),\n $this->createMock(AskAnythingRepository::class),\n );\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(false);\n $report->method('getRecipients')->willReturn(['users' => [5]]);\n $report->method('getJiminnyRecipients')->willReturn(['users' => []]);\n $report->method('getGroups')->willReturn([]);\n\n $result = $service->getValidRecipientUsers($report);\n\n $this->assertCount(1, $result);\n $this->assertEquals('user@test.com', $result[0]['email']);\n }\n\n public function testGetReportPeriodNameAskJiminnyMonthlyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-03-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertMatchesRegularExpression('/^[A-Z][a-z]+ \\d{4}$/', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWeeklyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('weekly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyDailyFallback(): void\n {\n Carbon::setTestNow(Carbon::parse('2026-04-07 00:00:00'));\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(null);\n $mockResult->method('getToDate')->willReturn(null);\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertStringNotContainsString(' - ', $result);\n $this->assertStringContainsString('2026', $result);\n\n Carbon::setTestNow();\n }\n\n public function testGetReportPeriodNameAskJiminnyWithExplicitDates(): void\n {\n $report = $this->createMock(AutomatedReport::class);\n $report->method('getFrequency')->willReturn('monthly');\n $report->method('isAskJiminnyReport')->willReturn(true);\n\n $mockResult = $this->createMock(AutomatedReportResult::class);\n $mockResult->method('getReport')->willReturn($report);\n $mockResult->method('getFromDate')->willReturn(IlluminateCarbon::parse('2026-02-07'));\n $mockResult->method('getToDate')->willReturn(IlluminateCarbon::parse('2026-03-07'));\n\n $result = $this->service->getReportPeriodName($mockResult);\n\n $this->assertEquals('Feb 2026', $result);\n }\n\n public function testGetReportThrowsWhenNotFound(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->with('missing-uuid')\n ->willReturn(null);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('missing-uuid');\n }\n\n public function testGetReportWithDefaultPartnerSkipsPartnerCheck(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $report = new AutomatedReport();\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(true);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMatchingPartnerIdReturnsReport(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $partnerId = 5;\n $team = new Team();\n $team->partner_id = $partnerId;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn($partnerId);\n\n $result = $this->service->getReport('some-uuid', $partner);\n $this->assertSame($report, $result);\n }\n\n public function testGetReportWithNonDefaultPartnerAndMismatchedPartnerIdThrows(): void\n {\n $reflection = new \\ReflectionClass(AutomatedReportsService::class);\n $property = $reflection->getProperty('automatedReportsRepository');\n $property->setAccessible(true);\n /** @var AutomatedReportsRepository&\\PHPUnit\\Framework\\MockObject\\MockObject $mockRepo */\n $mockRepo = $property->getValue($this->service);\n\n $team = new Team();\n $team->partner_id = 5;\n\n $report = new AutomatedReport();\n $report->setRelation('team', $team);\n\n $mockRepo->expects($this->once())\n ->method('findByUuid')\n ->willReturn($report);\n\n $partner = $this->createMock(Partner::class);\n $partner->method('isDefaultPartner')->willReturn(false);\n $partner->method('getId')->willReturn(2);\n\n $this->expectException(ModelNotFoundException::class);\n $this->service->getReport('some-uuid', $partner);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5288417764450564763
|
-1412888448243747103
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Carbon\Carbon;
use Illuminate\Support\Carbon as IlluminateCarbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Illuminate\Support\Collection;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Group;
use Jiminny\Models\Partner;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Jiminny\Services\Kiosk\AutomatedReports\ActivityTypeService;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\DealStagesService;
use Jiminny\Services\Kiosk\AutomatedReports\RecipientsService;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class AutomatedReportsServiceTest extends TestCase
{
private AutomatedReportsService $service;
protected function setUp(): void
{
parent::setUp();
// Create a real instance of the service without calling the constructor
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$this->service = $reflection->newInstanceWithoutConstructor();
// Manually set the dependencies using reflection
$dependencies = [
'teamRepository' => TeamRepository::class,
'groupRepository' => GroupRepository::class,
'userRepository' => UserRepository::class,
'stageRepository' => StageRepository::class,
'dealStagesService' => DealStagesService::class,
'recipientsService' => RecipientsService::class,
'automatedReportsRepository' => AutomatedReportsRepository::class,
'webhookService' => Webhook::class,
'dispatcher' => Dispatcher::class,
'activityTypeService' => ActivityTypeService::class,
'playbookCategoryRepository' => PlaybookCategoryRepository::class,
'askAnythingPromptService' => AskAnythingPromptService::class,
'activitySearchRepository' => SearchRepository::class,
'askAnythingRepository' => AskAnythingRepository::class,
];
foreach ($dependencies as $propertyName => $class) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($this->service, $this->createMock($class));
}
}
protected function tearDown(): void
{
parent::tearDown();
Mockery::close();
}
private function getService(
$mockUserRepository = null,
$mockStageRepository = null,
$mockTeamRepository = null,
$mockRepo = null,
): AutomatedReportsService {
return new AutomatedReportsService(
($mockTeamRepository ?? $this->createMock(TeamRepository::class)),
$this->createMock(GroupRepository::class),
($mockUserRepository ?? $this->createMock(UserRepository::class)),
($mockStageRepository ?? $this->createMock(StageRepository::class)),
$this->createMock(DealStagesService::class),
$this->createMock(RecipientsService::class),
($mockRepo ?? $this->createMock(AutomatedReportsRepository::class)),
$this->createMock(Webhook::class),
$this->createMock(Dispatcher::class),
$this->createMock(ActivityTypeService::class),
$this->createMock(PlaybookCategoryRepository::class),
$this->createMock(AskAnythingPromptService::class),
$this->createMock(SearchRepository::class),
$this->createMock(AskAnythingRepository::class),
);
}
#[DataProvider('transformMediaTypesDataProvider')]
public function testTransformMediaTypes(array $mediaTypes, array $expected): void
{
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$reflection = new \ReflectionClass(AutomatedReportsService::class);
$method = $reflection->getMethod('transformMediaTypes');
$result = $method->invoke($this->service, $report);
$this->assertEquals($expected, $result);
}
public function testGetMediaTypeFieldDataWithoutReport(): void
{
$result = $this->service->getMediaTypeFieldData(null);
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEmpty($result['value']);
$this->assertEquals('media_types', $result['id']);
}
public function testGetMediaTypeFieldDataWithReport(): void
{
$mediaTypes = ['pdf', 'podcast'];
$report = new AutomatedReport(['media_types' => $mediaTypes]);
$result = $this->service->getMediaTypeFieldData($report);
$expectedValue = [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
];
$this->assertIsArray($result);
$this->assertArrayHasKey('value', $result);
$this->assertEquals($expectedValue, $result['value']);
}
public static function transformMediaTypesDataProvider(): array
{
return [
'empty array' => [
'mediaTypes' => [],
'expected' => [],
],
'pdf only' => [
'mediaTypes' => ['pdf'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
],
],
'podcast only' => [
'mediaTypes' => ['podcast'],
'expected' => [
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'both pdf and podcast' => [
'mediaTypes' => ['pdf', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
'with invalid type' => [
'mediaTypes' => ['pdf', 'invalid', 'podcast'],
'expected' => [
['id' => 'pdf', 'name' => 'PDF'],
['id' => 'podcast', 'name' => 'Podcast'],
],
],
];
}
#[DataProvider('hasCallTypeConferenceDataProvider')]
public function testHasCallTypeConference(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeConference($report);
$this->assertEquals($expected, $result);
}
#[DataProvider('hasCallTypeDialerDataProvider')]
public function testHasCallTypeDialer(array $callTypes, bool $expected): void
{
$report = $this->createMock(AutomatedReport::class);
$report->method('getCallTypes')->willReturn($callTypes);
$result = $this->service->hasCallTypeDialer($report);
$this->assertEquals($expected, $result);
}
public static function hasCallTypeConferenceDataProvider(): array
{
return [
'has conference' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have conference' => [
'callTypes' => ['dialer', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public static function hasCallTypeDialerDataProvider(): array
{
return [
'has dialer' => [
'callTypes' => ['conference', 'dialer'],
'expected' => true,
],
'does not have dialer' => [
'callTypes' => ['conference', 'other'],
'expected' => false,
],
'empty call types' => [
'callTypes' => [],
'expected' => false,
],
];
}
public function testTransformReportResultsWithEmptyCollection(): void
{
$emptyCollection = new Collection([]);
$result = $this->service->transformReportResults($emptyCollection);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testTransformReportResultsStructure(): void
{
// Create a mock AutomatedReportResult with minimal setup to test structure
$mockReportResult = $this->createMockReportResult();
$collection = new Collection([$mockReportResult]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(1, $result);
$transformedResult = $result[0];
// Verify all expected keys are present
$expectedKeys = [
'id', 'name', 'frequency', 'recipients',
'report_type', 'media_type', 'downloadUrl', 'viewUrl', 'generated_at',
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, $transformedResult);
}
// Verify structure of nested arrays
$this->assertIsArray($transformedResult['frequency']);
$this->assertArrayHasKey('id', $transformedResult['frequency']);
$this->assertArrayHasKey('name', $transformedResult['frequency']);
$this->assertIsArray($transformedResult['report_type']);
$this->assertArrayHasKey('id', $transformedResult['report_type']);
$this->assertArrayHasKey('name', $transformedResult['report_type']);
$this->assertIsArray($transformedResult['recipients']);
// Verify TODO fields are null as expected
$this->assertEquals(AutomatedReportsService::MEDIA_TYPE_PODCAST, $transformedResult['media_type']);
$this->assertEquals(route('ai-reports.audio.download', ['uuid' => 'test-uuid']), $transformedResult['downloadUrl']);
$this->assertEquals(route('ai-reports.audio.view', ['uuid' => 'test-uuid']), $transformedResult['viewUrl']);
}
public function testTransformReportResultsWithMultipleResults(): void
{
$mockReportResult1 = $this->createMockReportResult('result-uuid-1', 'exec_summary');
$mockReportResult2 = $this->createMockReportResult('result-uuid-2', 'coaching_profiles');
$collection = new Collection([$mockReportResult1, $mockReportResult2]);
$result = $this->service->transformReportResults($collection);
$this->assertIsArray($result);
$this->assertCount(2, $result);
// Verify different UUIDs
$this->assertEquals('result-uuid-1', $result[0]['id']);
$this->assertEquals('result-uuid-2', $result[1]['id']);
// Verify both results have the expected structure
foreach ($result as $transformedResult) {
$this->assertArrayHasKey('id', $transformedResult);
$this->assertArrayHasKey('name', $transformedResult);
$this->assertArrayHasKey('frequency', $transformedResult);
$this->assertArrayHasKey('recipients', $transformedResult);
$this->assertArrayHasKey('report_type', $transformedResult);
}
}
#[DataProvider('isUserRecipientOfReportDataProvider')]
public function testIsUserRecipientOfReport(int $userId, array $recipients, bool $expected): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn(null);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertEquals($expected, $result);
}
#[DataProvider('isUserRecipientOfAskJiminnyReportDataProvider')]
public function testIsUserRecipientOfAskJiminnyReportViaGroup(
int $userId,
?int $groupId,
array $recipients,
array $reportGroups,
bool $expected,
): void {
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn($userId);
$mockUser->method('getGroupId')->willReturn($groupId);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$mockReport->method('isAskJiminnyReport')->willReturn(true);
$mockReport->method('getGroups')->willReturn($reportGroups);
$this->assertSame($expected, $this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public function testIsUserRecipientOfNonAskJiminnyReportIgnoresGroups(): void
{
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
$mockUser->method('getGroupId')->willReturn(5);
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => []]);
$mockReport->method('isAskJiminnyReport')->willReturn(false);
$mockReport->method('getGroups')->willReturn([5]);
$this->assertFalse($this->service->isUserRecipientOfReport($mockUser, $mockReport));
}
public static function isUserRecipientOfAskJiminnyReportDataProvider(): array
{
return [
'group member - ask jiminny' => [
'userId' => 123,
'groupId' => 7,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => true,
],
'group mismatch - ask jiminny' => [
'userId' => 123,
'groupId' => 9,
'recipients' => ['users' => []],
'reportGroups' => [7, 8],
'expected' => false,
],
'user with no group - ask jiminny' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => []],
'reportGroups' => [7],
'expected' => false,
],
'recipient users take precedence over group' => [
'userId' => 123,
'groupId' => null,
'recipients' => ['users' => [123]],
'reportGroups' => [],
'expected' => true,
],
];
}
public function testIsUserRecipientOfReportWithEmptyRecipients(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with no recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public function testIsUserRecipientOfReportWithNoUsersKey(): void
{
// Create mock User
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getId')->willReturn(123);
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [456, 789]]);
$result = $this->service->isUserRecipientOfReport($mockUser, $mockReport);
$this->assertFalse($result);
}
public static function isUserRecipientOfReportDataProvider(): array
{
return [
'user is recipient - single user' => [
'userId' => 123,
'recipients' => ['users' => [123]],
'expected' => true,
],
'user is recipient - multiple users' => [
'userId' => 456,
'recipients' => ['users' => [123, 456, 789]],
'expected' => true,
],
'user is not recipient - single user' => [
'userId' => 999,
'recipients' => ['users' => [123]],
'expected' => false,
],
'user is not recipient - multiple users' => [
'userId' => 999,
'recipients' => ['users' => [123, 456, 789]],
'expected' => false,
],
'user is recipient - string IDs converted to int' => [
'userId' => 123,
'recipients' => ['users' => ['123', '456']],
'expected' => true,
],
'user is not recipient - string IDs converted to int' => [
'userId' => 999,
'recipients' => ['users' => ['123', '456']],
'expected' => false,
],
'empty users array' => [
'userId' => 123,
'recipients' => ['users' => []],
'expected' => false,
],
];
}
private function createMockReportResult(string $uuid = 'test-uuid', string $reportType = 'exec_summary'): AutomatedReportResult
{
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getFrequency')->willReturn('weekly');
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2]]);
$mockReport->method('getGroups')->willReturn([10, 20]);
$mockReport->method('getType')->willReturn($reportType);
// Create mock Team
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock Group
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn('group-uuid-10');
$mockGroup->method('getName')->willReturn('Test Team');
$mockQueryBuilder = Mockery::mock();
$mockQueryBuilder->shouldReceive('where')->andReturnSelf();
$mockQueryBuilder->shouldReceive('first')->andReturn($mockGroup);
$dataRelation = Mockery::mock(HasMany::class);
$dataRelation->shouldReceive('where')->andReturn($mockQueryBuilder);
$dataRelation->shouldReceive('get')->andReturn(
new \Illuminate\Database\Eloquent\Collection([$mockGroup])
);
$mockTeam->method('groups')->willReturn($dataRelation);
$mockReport->method('getTeam')->willReturn($mockTeam);
// Create mock AutomatedReportResult
$mockReportResult = $this->createMock(AutomatedReportResult::class);
$mockReportResult->method('getUuid')->willReturn($uuid);
$mockReportResult->method('getGeneratedAt')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15T10:30:00Z')
);
$mockReportResult->method('getReport')->willReturn($mockReport);
// Mock methods used in getReportFileName
$mockReportResult->method('getReportType')->willReturn($reportType);
$mockReportResult->method('getFromDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-08')
);
$mockReportResult->method('getToDate')->willReturn(
\Illuminate\Support\Carbon::parse('2024-01-15')
);
$mockReportResult->method('getGroups')->willReturn([10]);
$mockReportResult->method('getMediaType')->willReturn(AutomatedReportsService::MEDIA_TYPE_PODCAST);
return $mockReportResult;
}
#[DataProvider('getUsersUuidsDataProvider')]
public function testGetUsersUuids(array $recipients, array $mockUsers, array $expectedUuids): void
{
// Create mock UserRepository
$mockUserRepository = $this->createMock(UserRepository::class);
// Configure the mock to return specific users for specific IDs using a callback
$mockUserRepository->method('find')
->willReturnCallback(function ($userId) use ($mockUsers) {
if (! isset($mockUsers[$userId])) {
return null;
}
$userUuid = $mockUsers[$userId]['uuid'] ?? null;
if ($userUuid === null) {
return null;
}
$mockUser = $this->createMock(\Jiminny\Models\User::class);
$mockUser->method('getUuid')->willReturn((string) $userUuid);
return $mockUser;
});
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn($recipients);
$result = $automatedReportsService->getUsersUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetUsersUuidsWithEmptyRecipients(): void
{
// Create mock AutomatedReport with empty recipients
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn([]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNoUsersKey(): void
{
// Create mock AutomatedReport with recipients but no 'users' key
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['other_key' => [1, 2, 3]]);
$result = $this->service->getUsersUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetUsersUuidsWithNonExistentUsers(): void
{
// Create mock UserRepository that returns null for all users
$mockUserRepository = $this->createMock(UserRepository::class);
$mockUserRepository->method('find')->willReturn(null);
// Create service with mocked UserRepository
$automatedReportsService = $this->getService(mockUserRepository: $mockUserRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getRecipients')->willReturn(['users' => [1, 2, 3]]);
$result = $automatedReportsService->getUsersUuids($mockReport);
// Should return array with null values for non-existent users
$this->assertEquals([], $result);
}
public static function getUsersUuidsDataProvider(): array
{
return [
'single user found' => [
'recipients' => ['users' => [123]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
],
'expectedUuids' => ['user-uuid-123'],
],
'multiple users found' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
456 => ['id' => 456, 'uuid' => 'user-uuid-456'],
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-456', 'user-uuid-789'],
],
'mixed found and not found users' => [
'recipients' => ['users' => [123, 456, 789]],
'mockUsers' => [
123 => ['id' => 123, 'uuid' => 'user-uuid-123'],
// 456 not found in DB
789 => ['id' => 789, 'uuid' => 'user-uuid-789'],
],
'expectedUuids' => ['user-uuid-123', 'user-uuid-789'], // Updated to reflect that nulls are filtered out
],
'empty users array' => [
'recipients' => ['users' => []],
'mockUsers' => [],
'expectedUuids' => [],
],
'all users not found' => [
'recipients' => ['users' => [123, 456]],
'mockUsers' => [], // No users found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getCurrentDealStagesUuidsDataProvider')]
public function testGetCurrentDealStagesUuids(array $currentDealStages, array $mockStages, array $expectedUuids): void
{
// Create mock StageRepository
$mockStageRepository = $this->createMock(StageRepository::class);
// Configure the mock to return specific stages for specific IDs using a callback
$mockStageRepository->method('find')
->willReturnCallback(function ($stageId) use ($mockStages) {
if (! isset($mockStages[$stageId])) {
return null;
}
$stageUuid = $mockStages[$stageId]['uuid'] ?? null;
if ($stageUuid === null) {
return null;
}
$mockStage = $this->createMock(\Jiminny\Models\Stage::class);
$mockStage->method('getUuid')->willReturn((string) $stageUuid);
return $mockStage;
});
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn($currentDealStages);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
$this->assertEquals($expectedUuids, $result);
}
public function testGetCurrentDealStagesUuidsWithEmptyStages(): void
{
// Create mock AutomatedReport with empty current deal stages
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([]);
$result = $this->service->getCurrentDealStagesUuids($mockReport);
$this->assertEquals([], $result);
}
public function testGetCurrentDealStagesUuidsWithNonExistentStages(): void
{
// Create mock StageRepository that returns null for all stages
$mockStageRepository = $this->createMock(StageRepository::class);
$mockStageRepository->method('find')->willReturn(null);
// Create service with mocked StageRepository
$automatedReportsService = $this->getService(mockStageRepository: $mockStageRepository);
// Create mock AutomatedReport
$mockReport = $this->createMock(AutomatedReport::class);
$mockReport->method('getCurrentDealStages')->willReturn([1, 2, 3]);
$result = $automatedReportsService->getCurrentDealStagesUuids($mockReport);
// Should return array with null values for non-existent stages
$this->assertEquals([], $result);
}
public static function getCurrentDealStagesUuidsDataProvider(): array
{
return [
'single stage found' => [
'currentDealStages' => [10],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
],
'expectedUuids' => ['stage-uuid-10'],
],
'multiple stages found' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
20 => ['id' => 20, 'uuid' => 'stage-uuid-20'],
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-20', 'stage-uuid-30'],
],
'mixed found and not found stages' => [
'currentDealStages' => [10, 20, 30],
'mockStages' => [
10 => ['id' => 10, 'uuid' => 'stage-uuid-10'],
// 20 not found in DB
30 => ['id' => 30, 'uuid' => 'stage-uuid-30'],
],
'expectedUuids' => ['stage-uuid-10', 'stage-uuid-30'], // Updated to reflect that nulls are filtered out
],
'empty stages array' => [
'currentDealStages' => [],
'mockStages' => [],
'expectedUuids' => [],
],
'all stages not found' => [
'currentDealStages' => [10, 20],
'mockStages' => [], // No stages found
'expectedUuids' => [], // Updated to reflect that nulls are filtered out
],
];
}
#[DataProvider('getTeamGroupsDataProvider')]
public function testGetTeamGroups(string $teamUuid, ?array $mockTeamData, array $mockGroups, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
if ($mockTeamData === null) {
// Team not found
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn(null);
} else {
// Team found - create mock team with groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create mock groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
// Create mock Group objects
$groupObjects = [];
foreach ($mockGroups as $groupData) {
$mockGroup = $this->createMock(\Jiminny\Models\Group::class);
$mockGroup->method('getUuid')->willReturn($groupData['id']);
$mockGroup->method('getName')->willReturn($groupData['name']);
$groupObjects[] = $mockGroup;
}
// Mock the groups collection to return our mock groups
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator($groupObjects));
// Mock the groups() relation
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
$mockTeamRepository->method('idOrUuid')
->with($teamUuid)
->willReturn($mockTeam);
}
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups($teamUuid);
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamGroupsWithNonExistentTeam(): void
{
// Create mock TeamRepository that returns null (team not found)
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn(null);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('non-existent-team-uuid');
$this->assertEquals([], $result);
}
public function testGetTeamGroupsWithEmptyGroups(): void
{
// Create mock team with no groups
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
// Create empty groups collection
$mockGroupsCollection = $this->createMock(\Illuminate\Database\Eloquent\Collection::class);
$mockGroupsCollection->method('getIterator')->willReturn(new \ArrayIterator([]));
$mockGroupsRelation = $this->createMock(\Illuminate\Database\Eloquent\Relations\HasMany::class);
$mockGroupsRelation->method('get')->willReturn($mockGroupsCollection);
$mockTeam->method('groups')->willReturn($mockGroupsRelation);
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('idOrUuid')->willReturn($mockTeam);
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeamGroups('team-with-no-groups');
$this->assertEquals([], $result);
}
public static function getTeamGroupsDataProvider(): array
{
return [
'team with single group' => [
'teamUuid' => 'team-uuid-123',
'mockTeamData' => ['id' => 'team-uuid-123', 'name' => 'Test Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
],
],
'team with multiple groups' => [
'teamUuid' => 'team-uuid-456',
'mockTeamData' => ['id' => 'team-uuid-456', 'name' => 'Another Team'],
'mockGroups' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
'expectedResult' => [
['id' => 'group-uuid-1', 'name' => 'Sales Team'],
['id' => 'group-uuid-2', 'name' => 'Marketing Team'],
['id' => 'group-uuid-3', 'name' => 'Support Team'],
],
],
'team not found' => [
'teamUuid' => 'non-existent-uuid',
'mockTeamData' => null,
'mockGroups' => [],
'expectedResult' => [],
],
'team with no groups' => [
'teamUuid' => 'team-uuid-empty',
'mockTeamData' => ['id' => 'team-uuid-empty', 'name' => 'Empty Team'],
'mockGroups' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('getTeamsDataProvider')]
public function testGetTeams(array $mockTeams, array $expectedResult): void
{
// Create mock TeamRepository
$mockTeamRepository = $this->createMock(TeamRepository::class);
// Create mock Team objects
$teamObjects = [];
foreach ($mockTeams as $teamData) {
$mockTeam = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam->method('getUuid')->willReturn($teamData['id']);
$mockTeam->method('getName')->willReturn($teamData['name']);
$mockTeam->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn($teamData['hasAutomatedReports']);
$teamObjects[] = $mockTeam;
}
// Mock the repository to return a Collection (not array)
$mockTeamRepository->method('getTeamsForKiosk')
->with('active')
->willReturn(new Collection($teamObjects));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals($expectedResult, $result);
}
public function testGetTeamsWithNoTeams(): void
{
// Create mock TeamRepository that returns empty Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsWithAllTeamsWithoutFeature(): void
{
// Create mock teams without AUTOMATED_REPORTS feature
$mockTeam1 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam1->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
$mockTeam2 = $this->createMock(\Jiminny\Models\Team::class);
$mockTeam2->method('hasFeature')
->with(\Jiminny\Models\Feature\FeatureEnum::AUTOMATED_REPORTS)
->willReturn(false);
// Create mock TeamRepository that returns Collection
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->method('getTeamsForKiosk')->willReturn(new Collection([$mockTeam1, $mockTeam2]));
// Create service with mocked TeamRepository
$automatedReportsService = $this->getService(mockTeamRepository: $mockTeamRepository);
$result = $automatedReportsService->getTeams();
$this->assertEquals([], $result);
}
public function testGetTeamsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeams(partner: $partner);
}
public function testGetOrganizationFieldDataForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getOrganizationFieldData(partner: $partner);
}
public function testGetTeamsGroupsOptionsForwardsPartnerToRepository(): void
{
$partner = $this->createMock(Partner::class);
$mockTeamRepository = $this->createMock(TeamRepository::class);
$mockTeamRepository->expects(self::once())
->method('getTeamsForKiosk')
->with(Team::STATUS_ACTIVE, $partner)
->willReturn(new Collection([]));
$service = $this->getService(mockTeamRepository: $mockTeamRepository);
$service->getTeamsGroupsOptions(partner: $partner);
}
public static function getTeamsDataProvider(): array
{
return [
'single team with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
],
],
'multiple teams with feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-2', 'name' => 'Marketing Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'mixed teams - some with feature, some without' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => true,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-3',
'name' => 'Support Team',
'hasAutomatedReports' => true,
],
],
'expectedResult' => [
['id' => 'team-uuid-1', 'name' => 'Sales Team'],
['id' => 'team-uuid-3', 'name' => 'Support Team'],
],
],
'all teams without feature' => [
'mockTeams' => [
[
'id' => 'team-uuid-1',
'name' => 'Sales Team',
'hasAutomatedReports' => false,
],
[
'id' => 'team-uuid-2',
'name' => 'Marketing Team',
'hasAutomatedReports' => false,
],
],
'expectedResult' => [],
],
'empty teams array' => [
'mockTeams' => [],
'expectedResult' => [],
],
];
}
#[DataProvider('deleteS3FilesDataProvider')]
public function testDeleteS3Files(
string $mediaType,
array $expectedFileExtensions,
array $existingFiles,
string $pathSuffix,
int $expectedDeletes
): void {
// Arrange
$teamUuid = 'team-uuid-123';
$reportUuid = 'report-uuid-456';
$basePath = sprintf('%s/reports/%s', $teamUuid, $reportUuid);
$team = Mockery::mock(Team::class);
$team->allows('getUuid')->andReturn($teamUuid);
$report = Mockery::mock(AutomatedReport::class);
$report->allows('getTeam')->andReturn($team);
$result = Mockery::mock(AutomatedReportResult::class);
$result->allows('getReport')->andReturn($report);
$result->allows('getUuid')->andReturn($reportUuid);
$result->allows('getMediaType')->andReturn($mediaType);
Storage::fake();
Log::shouldReceive('info')->times($expectedDeletes);
foreach ($existingFiles as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
Storage::put($filePath, 'dummy content');
}
// Act
$this->service->deleteS3Files($result);
// Assert
foreach ($expectedFileExtensions as $extension) {
$filePath = $basePath . $pathSuffix . '.' . $extension;
if (in_array($extension, $existingFiles, true)) {
Storage::assertMissing($filePath);
} else {
// To be sure no unexpected files were created and deleted
Storage::assertMissing($filePath);
}
}
}
public static function deleteS3FilesDataProvider(): array
{
return [
'PDF report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'MD', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 3,
],
'PDF report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => ['html', 'pdf'],
'pathSuffix' => '',
'expectedDeletes' => 2,
],
'PDF report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PDF,
'expectedFileExtensions' => ['html', 'MD', 'pdf'],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
'Podcast report, all files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['json', 'mp3', 'ssml'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 3,
],
'Podcast report, some files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => ['mp3'],
'pathSuffix' => '_podcast',
'expectedDeletes' => 1,
],
'Podcast report, no files exist' => [
'mediaType' => AutomatedReportsService::MEDIA_TYPE_PODCAST,
'expectedFileExtensions' => ['json', 'mp3', 'ssml'],
'existingFiles' => [],
'pathSuffix' => '_podcast',
'expectedDeletes' => 0,
],
'Other media type, should do nothing' => [
'mediaType' => 'some_other_type',
'expectedFileExtensions' => [],
'existingFiles' => [],
'pathSuffix' => '',
'expectedDeletes' => 0,
],
];
}
public function testDeleteReportsResultsInRetentionPeriodWithLogging(): void
{
// Create mocks for the test
$automatedReportsService = Mockery::mock(AutomatedReportsService::class);
$team = Mockery::mock(Team::class);
$team->shouldReceive('getId')->andReturn(123);
$from = now()->subDays(30);
$to = now();
$source = 'test-source';
// Expect the method to be called with specific parameters
$automatedReportsService->shouldReceive('deleteReportsResultsInRetentionPeriodWithLogging')
->once()
->with(
$team,
Mockery::on(function ($arg) use ($from) {
return $arg->timestamp === $from->timestamp;
}),
Mockery::on(function ($arg) use ($to) {
return $arg->timestamp === $to->timestamp;
}),
$source
)
->andReturn(5);
// Call the method and verify the result
$result = $automatedReportsService->deleteReportsResultsInRetentionPeriodWithLogging(
$team,
$from,
$to,
$source
);
$this->assertEquals(5, $result);
}
#[DataProvider('sanitizeFileNameDataProvider')]
public function testSanitizeFileName(string $input, string $expected): void
{
$result = $this->service->sanitizeFileName($input);
$this->assertEquals($expected, $result);
}
public static function sanitizeFileNameDataProvider(): array
{
return [
'no special characters' => [
'input' => 'Exec Summary - Sep 2025 - Business Development Team',
'expected' => 'Exec Summary - Sep 2025 - Business Development Team',
],
'forward slash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND/IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'backslash in team name' => [
'input' => 'Exec Summary - Sep 2025 - ND\IRV',
'expected' => 'Exec Summary - Sep 2025 - ND-IRV',
],
'multiple forward slashes' => [
'input' => 'Report - Team A/B/C',
'expected' => 'Report - Team A-B-C',
],
'multiple backslashes' => [
'input' => ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62696
|
2221
|
16
|
2026-05-20T11:58:48.324452+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278328324_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.375,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.38663563,"top":0.15003991,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"73","depth":4,"bounds":{"left":0.4005984,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3074361448870484652
|
225199225622834114
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
14
102
73
Previous Highlighted Error
Next Highlighted Error
PhostormINavigarecodeWindowFV faVsco.js#12098 on JY-20676-delete-report-related-obProledey© AskAnythingController.php> • OpportunitySyncStrategy• • ProspectSearchStrategy> N ServiceTraits© ClientTest.phpC) DecorateActivitylest.pnpC DeleteObjectsTraitTest.phpC FieldDefinitionsTest.php© GetActivityFieldNameTest.phpPayloadBuilderTest.php@ QueryBuilderTest.phpC) QueryHandlerTest.php© QuerylteratorTest.php© QuervResultsTests.php© AskAnythingPromptService.php© AutomatedReportsServiceTest.php X© AskAnythingPromptDto.php© AutomatedReport.php© AskJiminnyReportsController.phg© AutomatedReportsService.php) AskAnythingPromptServiceTest.php(c) service lest.onoC SvncBatchRedisServiceTest.php©) BaseServiceTest.oho©) CrmActivitvServicelest.onoc) crmobiectsResolveriiest.onoc DefaultProsnectSearchStrateavTest.cc EmailHelperTest.oho3057 %© FieldValueConverterTest.phpc LavoutManaderTest.oho3123 %© MigrateProviderServiceTest.php© OpportunityActivityMatcherTest.php3150 %© OpportunitySyncStrategyResolverTes© ProspectCacheTest.php3177 %© ProspectSearchStrategyFactoryTest.p© ProviderRegistryTest.php© RecordSelectorTest.php@PocalvaComnanuNamoBuEmalTraiT, 32333204 P3234 P© TimePerioditeratorTest.php© UpdateCrmDataResolverTest.php3264 Ф> C Internalv @ Kioskv @ AutomatedReports13294 V.l© ActivitvTvpeServiceTest.php© AskJiminnyReportActivitvServiceTa© AskJiminnyReportServiceTest.php@ AutomatedReportsCallbackServiceC AutomatedReportsServiceActivitie.@ AutomatedReportsServiceExtende3612 V5@ AutomatedReportsServiceFieldDat 943(C) AutomatedRenortsServiceFormatti@ AutomatedReportsServiceGetActivlC AutomatedRenortsServiceGetPlavt( AutomatedRenortcServiceRenortCCh AutomatedRenortsServiceValidatioclass AutomatedReportsservicelest extends Testcasepublic function testUpdateASkJiminnyReportStatusSuccess(): voidsmockrevort->exoectssth1s->onceob->method' rresh")->wilReturn(SmockFreshReoort):Sservice = Sthis->aetService mockRepo: SmockReoo):Sresult = $service->updateAskJiminnyReportStatus($mockReport, true):$this->assertSame($mockFreshReport, $result):public function testGetAskJiminnyReportFilters: void{...}public function testGetAskJiminnyReportFormDataWithoutReport: void{...}public function testGetAskJiminnyReportFormDataWithReport: void{...]public function testValidateAskJiminnyReportDataMissingName: void{...}public function testValidateAskJiminnyReportDataNameTooLong: void{...}public function testValidateAskJiminnyReportDataInvalidFrequency: void{...}public function testValidateAskJiminnvReportDataMissingExpires0n@: voidf...}public function testValidateAskJiminnvReportDataExpiresInPast@: void{...}public function testValidateAskJiminnvReportDataExpiresTooFar@: voidf...}pubizo functzon testVaL_dateA5KJminyReportDataExpiresExactZy0neYearLaterTáme0fDayTsAccepted() : y239public function testValidateAskJiminnyReportDataMissingSavedSearch(): void{...}public function testValidateAskJiminnyReportDataSavedSearchNotFound(): void{...}public function testValidateAskJiminnyReportDataMissingPrompt(): voidf...}public function testValidateAskJiminnyReportDataPromptNotFound(): voidf...}public function testTransformRecipientsWithNullUsers(0: void{...}public function testTransformRecipientsWithUsersKey0: void{...}public function testGetTeamsGroupsOptions: voidf...}=custom.log© Search.phpA14 A102 ×73 ^ V 208207209- 210=211212=214—211=217=219=222=223=22= 229230— 27₴-234236238242=249= laravel.log4 SF jiminny@localhost] X4 HS_local [jiminny@localhost]& console [PROD]# console [euyPlavaroundvSo jiminny vm 021 A1 A18 V2 Y6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated_reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm_configurations where id = 500:select * from users where name = 'Integration Account'; # 1695SELECT * FROMsocial accounts WHERE sociable id = 1695:select * from activities where crm confiquration id = 39and recording_state = 'recorded' and duration › 60and status='comoleted' and actual start time >=12025-12-011SsIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620560 = Wnd•splect * from leads.SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003):SELECT * FROM activities where status = 'failed':CEISCT + CP0M +nocke WHCPE activitu ¿d - 422007-csisoTSpnM antivitioc aJOIN users u 1..n<->1: ON a.user id = u.idMuEDea.status = 'completed'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks +LTMTT 25:• suppont Dally • In 2 m100% S2• Wed 20 May 14:58:48CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..BActivaityoatorromptservace-plpaelos . wilwYou're right to question this. Let me clarify:1. The hasReports Parameterlactuallv identified this as reauired in my review — it was listed as "Still Open" issue #4. The ed1t() method was missingthe 8th parameter, causina the frontend to receive no has renorts field after an edit. which breaks the delete quard forpromots that nave reporsWithout it: After editina a oromot that backs a report. has reports is missina → frontend talls back to false → deleteZ. Pertormance of loadCount('automatedReports')Imnact. Minimal - it eyecutes one additionall colNt(*) query.SFIECT GOUNT(*) FROM automated renortc WHFRF activity cearch id = ?• Indexed query — assumes activity search 1d has an index (Standard for toreign keys)• • Consistent with existing code — ListActivitvSearcho already uses withCount ('automatedReports') line 1100\• • Only on single fetch - fetchActivitySearch is for viewing one search, not list operations• V No N+1 — it's a sinqle querv. not per-itemAlternative considered: Eager loading the actual reports with load('automatedReports') would be heavier. loadCount()is the liahtweiaht ontion — it only aets the count. not the fulli modelsVerdict: The performance impact is negligible (~1-2ms for an indexed count), and it's necessary for correctness. The bug itfixes (delete quard silently failina) outweiahs the minimal query costRenorts/M AutomatedRenortcServiceTest.nhn +28-11:/M.AskAnvthinaPromotService.ohvAsk anvthing 884-D)< CodeC° Adantive* Reiect alliiAccent allW Windsurf Teamc1-1.UTE.8io 4 spaces...
|
62695
|
NULL
|
NULL
|
NULL
|
|
62695
|
2221
|
15
|
2026-05-20T11:58:39.521218+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278319521_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
43
3
10
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $acti...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.3693484,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.38164893,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.39162233,"top":0.15003991,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.40325797,"top":0.15003991,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.41223404,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4195479,"top":0.14844373,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9090646619974445669
|
-8385861013498784620
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
43
3
10
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $acti...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62694
|
2220
|
13
|
2026-05-20T11:58:39.158104+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278319158_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
43
3
10
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $acti...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9090646619974445669
|
-8385861013498784620
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
43
3
10
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $acti...
|
62689
|
NULL
|
NULL
|
NULL
|
|
62693
|
2221
|
14
|
2026-05-20T11:58:36.072663+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278316072_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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}]...
|
-398265953134711463
|
-9070248263785700415
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Phostorm1001SFV faVsco.js#12098 on JY-20676-delete-report-related-objects -Proiect© ActivityController.php Xphp api.php© AskAnythingController.php© AskAnythingPromptService.phpAsKAnytIngkepository.ong© AutomatedReportsServiceTest.phpAutomatedReports_ calendarsD Crm© AskAnythingPromptDto.php© AskJiminnyReportsController.php© AutomatedReportsService.php) AskAnythingPromptServiceTest.phpW clasticsearch_ GroupsC Import_NudaesD OpportunitiesPlaybooksSidekick?string Sprefix = nuluu TranscriptionD: self {...}Musers11865C Event. oho@ EventDispatcher.ohp• M ExcentionsD FFMpegIM FormatsO Guards• M HelnercD Http1M AccoccTokenProvider1076107110721973D ControllersVMAD1075MAiCallScoring1076_ AKeports1072> CJ DeallnsightsOoporunity› 0 Page1079• Scorecards›D Settinas• C TeamInsights›D Themes• _ UserAutomatedRevorts© UserAutomatedReportsController.$ 1086v Mv2(C) ActivitvV2Controller.oho@ AskAnvthinaController.ohvC) Ask.JiminnvReoortsControlier.onoC) DealsV2Controller.oho10901991(C) OnDemandV2Controller.nhn©PlaylistController.phpC) PlavlistShareController.ohrtinal class ActivitycontrollerYrwwTw1992 1>% PlavlistTrackController.php11A8© TranscriptionSummaryController.pl 11ec© UploadController.php1116© ActionitemsController.phpC0 ActivitvControllor nhr© AiCrmNotesController.php© BaseController.phpprivate function storeNamedSearchFilters(colleccion srequesc,search ssearch,FilterDefinitionCollection SfilterSet.* achrows_AuthorzzationExcentionGET lapi/v1/activitv/saved-search/search}laoi.savedsearch.showioublic function fetchActivitvSearchdSearch Ssearch.Reauest SreauestSpanchtinansformen Scparchtinansfarmen): JsonResponse 1Sthis->authonized ability "viewl Sceanch)•/** @var User $user */Susen = Snequest->usend•cthie-snocnonco->getManagerO->setSerializer(new JsonSerializer0):ssearch->Loadcount relations'automatedReports'):return sth1s->response->w1thltemo->wathconsumer(Suser)* Neletes a saved cearch* Qnaram Reaues+ Sreauest* Athnowe Eycontior=custom.log= laravel.logA SF [jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING)© Search.php207208=[CREDIT_CARD]-2142119— 211217218122g227228Accept Reject— 230231232233234235-236237238239240—241242-243© CoachingFeedbackCoachUserln.phpTx: AutovriaygroundSo jiminny vLAIbl8XLY0 ASELECT * FROM automated_reports where id = 67;SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account'; # 1695SELECT * FROMsochal accounts WHERE soctable 10 = 16951select * from activities where crm confiquration id = 39and recording_state = 'recorded' and duration > 60and status='comoleted' and actual start time >=12025-12-011SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;splect * from leads.SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003):SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user_id = u.idWHEREa.status = 'completed'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidWHERE t.activitv id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESdLTMTT 25:select * from teams where id = 19;select * from com confiaurations whene nroviden = Ininedrivel.SEIECTCONCATEu #d CASE WHEN n id - + ounon #d THEN 1 (ounon)! EISE 11 ENO) AS ncon ¿dl+ ounon id EPOM cocial Accountc caOTM ucone mon mid = ca cociablo idsupoont Dally • In Zm• wea zu May 14:00.32CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..BActivatyConatorromptservace-plpaelos . wilYou're right to question this. Let me clarify:1. The hasReports ParameterI actually identified this as required in my review — it was listed as "Still Open" issue #4. The edit() method was missingthe 8th parameter, causina the frontend to receive no has reports field after an edit. which breaks the delete quard forpromots that nave reporsWithout it: After editina a oromot that backs a report. has reports is missina → frontend talls back to false → deleteZ. Pertormance of loadCount('automatedReports')Impact: Minimal - it executes one additional COUNT(*) query:• saSELECT COUNT(*) FROM automated reports WHERE activity search id = ?• Indexed query — assumes activity search 1d has an index (Standara tor toreign keys)• • Consistent with existing code — ListActivitvSearcho already uses withCount ('automatedReports') line 1100\• • Only on single fetch - fetchActivitySearch is for viewing one search, not list operations• V No N+1 — it's a sinqle querv. not per-itemAlternative considered: Eager loading the actual reports with load('automatedReports') would be heavier. loadCount()is the liahtweiaht ontion — it only aets the count. not the fulli modelsVerdict: The performance impact is negligible (~1-2ms for an indexed count), and it's necessary for correctness. The bug itfixes (delete quard silently failina) outweiahs the minimal query cost4 tiles with changes vnent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3norte/@ AutomatodDonortcGorvicoToct nhn 129-1/M.AskAnvthinaPromntService.ohn t1lllers/AP|/M ActivitvController.ohnAsk anvthing 884-D)< CodeC° Adantive* Reiect alliiAccent alli1092:18 /20 charcliPo. 4 space...
|
62690
|
NULL
|
NULL
|
NULL
|
|
62692
|
2221
|
13
|
2026-05-20T11:58:30.813417+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278310813_m2.jpg...
|
iTerm2
|
APP (docker)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.3882979,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39029256,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (docker)","depth":2,"bounds":{"left":0.5063165,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50831115,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6243351,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6263298,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7938220870340201062
|
-807278780399981859
|
idle
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
62690
|
NULL
|
NULL
|
NULL
|
|
62691
|
2220
|
12
|
2026-05-20T11:58:29.535536+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278309535_m1.jpg...
|
iTerm2
|
APP (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.24652778,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25069445,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (docker)","depth":2,"bounds":{"left":0.49305555,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49722221,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7395833,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.74375,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (docker)","depth":1,"bounds":{"left":0.47013888,"top":0.033333335,"width":0.0625,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7938220870340201062
|
-807278780399981859
|
idle
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
62689
|
NULL
|
NULL
|
NULL
|
|
62690
|
2221
|
12
|
2026-05-20T11:58:00.620541+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278280620_m2.jpg...
|
iTerm2
|
APP (docker)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.3882979,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39029256,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (docker)","depth":2,"bounds":{"left":0.5063165,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50831115,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6243351,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6263298,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-7938220870340201062
|
-807278780399981859
|
idle
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62689
|
2220
|
11
|
2026-05-20T11:57:59.444972+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278279444_m1.jpg...
|
iTerm2
|
APP (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.24652778,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25069445,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (docker)","depth":2,"bounds":{"left":0.49305555,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49722221,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7395833,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.74375,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (docker)","depth":1,"bounds":{"left":0.47013888,"top":0.033333335,"width":0.0625,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-7938220870340201062
|
-807278780399981859
|
idle
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (docker)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62687
|
2220
|
10
|
2026-05-20T11:57:29.336777+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278249336_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.24652778,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25069445,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.49305555,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49722221,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7395833,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.74375,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47569445,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-6117577997839782076
|
-807278780397753635
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (-zsh)...
|
62685
|
NULL
|
NULL
|
NULL
|
|
62688
|
2221
|
11
|
2026-05-20T11:57:29.323414+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278249323_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.3882979,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39029256,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.5063165,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50831115,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6243351,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6263298,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.49800533,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-6117577997839782076
|
-807278780397753635
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
screenpipe"
Close Tab
⌥⌘1
APP (-zsh)...
|
62686
|
NULL
|
NULL
|
NULL
|
|
62686
|
2221
|
10
|
2026-05-20T11:57:27.914378+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278247914_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
pmset
Close Tab
⌥⌘1
APP (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.3882979,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39029256,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.5063165,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.50831115,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"pmset","depth":2,"bounds":{"left":0.6243351,"top":1.0,"width":0.11801862,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6263298,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.49800533,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
7976089774813318872
|
-807278780397753635
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
pmset
Close Tab
⌥⌘1
APP (-zsh)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62685
|
2220
|
9
|
2026-05-20T11:57:27.899619+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278247899_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
pmset
Close Tab
⌥⌘1
APP (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.24652778,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25069445,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.49305555,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49722221,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"pmset","depth":2,"bounds":{"left":0.7395833,"top":0.05888889,"width":0.24652778,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.74375,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47569445,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
7976089774813318872
|
-807278780397753635
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
pmset
Close Tab
⌥⌘1
APP (-zsh)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62684
|
2221
|
9
|
2026-05-20T11:57:25.303884+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278245303_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
62680
|
NULL
|
NULL
|
NULL
|
|
62683
|
2220
|
8
|
2026-05-20T11:57:03.888107+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278223888_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","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}]...
|
-6884959928424888601
|
-8385843421312740204
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62682
|
2221
|
8
|
2026-05-20T11:56:54.794989+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278214794_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
62680
|
NULL
|
NULL
|
NULL
|
|
62681
|
2220
|
7
|
2026-05-20T11:56:33.557476+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278193557_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
62679
|
NULL
|
NULL
|
NULL
|
|
62680
|
2221
|
7
|
2026-05-20T11:56:24.172818+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278184172_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62679
|
2220
|
6
|
2026-05-20T11:56:01.991035+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278161991_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 4 mA100% <78• Wed 20 May 14:56:01T₴1|screenpipe"O ₴4DOCKER• 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
566879247022620255
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 4 mA100% <78• Wed 20 May 14:56:01T₴1|screenpipe"O ₴4DOCKER• 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62677
|
2220
|
5
|
2026-05-20T11:55:59.191786+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278159191_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)Support Daily - in 5 mA100% <8• Wed 20 May 14:55:59T₴1|screenpipe"O ₴4DOCKER₴81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)₴2APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
-5846214866962948258
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)Support Daily - in 5 mA100% <8• Wed 20 May 14:55:59T₴1|screenpipe"O ₴4DOCKER₴81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)₴2APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
62675
|
NULL
|
NULL
|
NULL
|
|
62678
|
2221
|
6
|
2026-05-20T11:55:59.157835+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278159157_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigarecodeLaravelKeractorJOOISWindo PhostormVIewINavigarecodeLaravelKeractorJOOISWindowmelpFV faVsco.js#12098 on JY-20676-delete-report-related-objects vProledeyCActivityController.ong© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php XC) AutomatedReportsServicelest.onpm EventsAsKAnytningPromptservice.ong©ASKAnythingPromptDto.pngc Historyservice.ongD AskJiminnyAiWAWSu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• EncryptionDES1M SakenD FeatureFlagsD FFMpegD FileSystemD Gong_ cuzzienutoC KeyPointsKIOSK_ LanquageDetectionW LOCKSW MathMedia?ioeline2 MobileSettinas/ Modela NudaelIM ParagranhBreaker1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrocnorWorkdM Auonc© AskJiminnyReportsController.phg© AutomatedReportsService.php©) AskAnythingPromptServiceTest.php© Search.php159160161162163164165166class ASkAnyth1ngPromptServ1cepublic function edit(if ($prompt->isDefaultPromptO) {$this->askAnythingRepository->hidePromptForUser($prompt, Suser);$newPrompt = $this->askAnythingRepository->createPrompt($user$prompt->getTargeto$titlescontent,$shareUsersssharebroups.elseif (Spromot->getownerido === suser->getiido) «$newPrompt = $this->askAnythingRepository->editPromptCSpromot.•else {throw new_InvalidAraumentExcentiong message"Edit not allowed for nromot ".$prompt->getUu$newPrompt->getUuido.SnewPromnt->aetTitle0lSnewPnomnt->aetContentO$newPrompt->getTargetOCucon.saotlluidtCchanollconcituidessharebroupsuulassnewrromor->cechaskeporcsoAccept ReiectCaccade 90 1=1Command 86• Extract Surroundpublic function deletelAskAnvthingPromot Sprompt.User suserAskAnvthanaPromotnublic function reorder(User Suserarrav Soromotluids.)• void s..}1Accept File &+X Reject File 4%€ + 1 of 4 files →=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]207208- -250251252253254© CoachingFeedbackCoachUserln.phpTx: AutovPlavaroundSo jiminny v021 A1 A18 V2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated_ reports where 10 = 421SELECT * FROM users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm contiqurations where 1d = 500:select * from users where name = "Integration Account"; # 1695SELE * FROMIsochal accounts WiERE soctable 10 = 16951select * fFrom activities where crm confiauration 1d = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•select * from leads;SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'comoleted'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks tWHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESCLTMTT 25•select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive':SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_idnomailsa.*,t.owner id FROM social accounts saOTM ucone non mid = ca cociablo idl• suppont Dally • In o m100% L2• Wed 20 May 14:55:58AutomatedReportsServiceTest ~CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..Note: The test name is also misleading - it tests updateAskJiminnyReport() (line 2877), not updateAskJiminnyReportStatuSO.3. Missing Success-Path Test for updateAskJiminnyReportStatus - NOT ADDEDcurrent Tests:•testUpdateAskJ1m1nnyReportstatusNotASKJ 1m1nny (misnamed, tests ditterent method)• testUodateAskJiminnvReportStatusThrowswhenEnabLinoWithMissinaReferences (error path)• testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling (disable path)Missing: Happy path where $status = true, isAsKJiminnyReport() = true, canExecute() = true → update() is calledand tresh is feturnedAedito Omits hacRenorts in DTO — NOT SIXEDILocation: AckAnvthinaPromntService.nbn lines 152-161=phpreturn new AskAnythingPromptDto(SnewPromnt-saetTitle().Tardetorsuser->getuuid),SchareGrounslluids// hasReports NOT PASSED → nullImnact: After an edit frontend receives no has renorts field causina the delete quard to he temnorarilv absent forFix: Pass Sprompt->getHasReports() (or false for newly created prompts in the default prompt branch) as the 8thparameter. matchina create line 104,5. fetchActivitySearch Silently Returns has reports: false - NOT FIXEDLocation: ActivityController.pho lines 1069-1088J UstActivitySearch (Uine 1100) - CORRECTISuser->searches ()->withCount('automatedReports')->get()return Schis→>response->wichltemSsearch. No withcount!View allnent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3Ponorte/M AutomatodDonortcGorvicoToct nhn 129-1ann/Comnonent/AskAnvthina/MAskAnvthinaPromntService.ohn t1lapo/Htto/Controllers/AP|/[m ActivitvController.ohvReiect alllAccent allibut orevious v vou mentioned thatI don't have to add @AskAnvthinaPromotService.ono#161l< CodeC° AdantiveW Windsurf Toams 161-12 /28 charc)UTE.8Po 4 spaces...
|
NULL
|
-5988529089659766714
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavigarecodeLaravelKeractorJOOISWindo PhostormVIewINavigarecodeLaravelKeractorJOOISWindowmelpFV faVsco.js#12098 on JY-20676-delete-report-related-objects vProledeyCActivityController.ong© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php XC) AutomatedReportsServicelest.onpm EventsAsKAnytningPromptservice.ong©ASKAnythingPromptDto.pngc Historyservice.ongD AskJiminnyAiWAWSu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• EncryptionDES1M SakenD FeatureFlagsD FFMpegD FileSystemD Gong_ cuzzienutoC KeyPointsKIOSK_ LanquageDetectionW LOCKSW MathMedia?ioeline2 MobileSettinas/ Modela NudaelIM ParagranhBreaker1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrocnorWorkdM Auonc© AskJiminnyReportsController.phg© AutomatedReportsService.php©) AskAnythingPromptServiceTest.php© Search.php159160161162163164165166class ASkAnyth1ngPromptServ1cepublic function edit(if ($prompt->isDefaultPromptO) {$this->askAnythingRepository->hidePromptForUser($prompt, Suser);$newPrompt = $this->askAnythingRepository->createPrompt($user$prompt->getTargeto$titlescontent,$shareUsersssharebroups.elseif (Spromot->getownerido === suser->getiido) «$newPrompt = $this->askAnythingRepository->editPromptCSpromot.•else {throw new_InvalidAraumentExcentiong message"Edit not allowed for nromot ".$prompt->getUu$newPrompt->getUuido.SnewPromnt->aetTitle0lSnewPnomnt->aetContentO$newPrompt->getTargetOCucon.saotlluidtCchanollconcituidessharebroupsuulassnewrromor->cechaskeporcsoAccept ReiectCaccade 90 1=1Command 86• Extract Surroundpublic function deletelAskAnvthingPromot Sprompt.User suserAskAnvthanaPromotnublic function reorder(User Suserarrav Soromotluids.)• void s..}1Accept File &+X Reject File 4%€ + 1 of 4 files →=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]207208- -250251252253254© CoachingFeedbackCoachUserln.phpTx: AutovPlavaroundSo jiminny v021 A1 A18 V2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated_ reports where 10 = 421SELECT * FROM users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm contiqurations where 1d = 500:select * from users where name = "Integration Account"; # 1695SELE * FROMIsochal accounts WiERE soctable 10 = 16951select * fFrom activities where crm confiauration 1d = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•select * from leads;SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'comoleted'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks tWHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESCLTMTT 25•select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive':SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_idnomailsa.*,t.owner id FROM social accounts saOTM ucone non mid = ca cociablo idl• suppont Dally • In o m100% L2• Wed 20 May 14:55:58AutomatedReportsServiceTest ~CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..Note: The test name is also misleading - it tests updateAskJiminnyReport() (line 2877), not updateAskJiminnyReportStatuSO.3. Missing Success-Path Test for updateAskJiminnyReportStatus - NOT ADDEDcurrent Tests:•testUpdateAskJ1m1nnyReportstatusNotASKJ 1m1nny (misnamed, tests ditterent method)• testUodateAskJiminnvReportStatusThrowswhenEnabLinoWithMissinaReferences (error path)• testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling (disable path)Missing: Happy path where $status = true, isAsKJiminnyReport() = true, canExecute() = true → update() is calledand tresh is feturnedAedito Omits hacRenorts in DTO — NOT SIXEDILocation: AckAnvthinaPromntService.nbn lines 152-161=phpreturn new AskAnythingPromptDto(SnewPromnt-saetTitle().Tardetorsuser->getuuid),SchareGrounslluids// hasReports NOT PASSED → nullImnact: After an edit frontend receives no has renorts field causina the delete quard to he temnorarilv absent forFix: Pass Sprompt->getHasReports() (or false for newly created prompts in the default prompt branch) as the 8thparameter. matchina create line 104,5. fetchActivitySearch Silently Returns has reports: false - NOT FIXEDLocation: ActivityController.pho lines 1069-1088J UstActivitySearch (Uine 1100) - CORRECTISuser->searches ()->withCount('automatedReports')->get()return Schis→>response->wichltemSsearch. No withcount!View allnent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3Ponorte/M AutomatodDonortcGorvicoToct nhn 129-1ann/Comnonent/AskAnvthina/MAskAnvthinaPromntService.ohn t1lapo/Htto/Controllers/AP|/[m ActivitvController.ohvReiect alllAccent allibut orevious v vou mentioned thatI don't have to add @AskAnvthinaPromotService.ono#161l< CodeC° AdantiveW Windsurf Toams 161-12 /28 charc)UTE.8Po 4 spaces...
|
62676
|
NULL
|
NULL
|
NULL
|
|
62675
|
2220
|
4
|
2026-05-20T11:55:51.759736+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278151759_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 5 mA100% [8• Wed 20 May 14:55:51T₴1|screenpipe"O ₴4DOCKER• 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
-5458386037559551588
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 5 mA100% [8• Wed 20 May 14:55:51T₴1|screenpipe"O ₴4DOCKER• 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62676
|
2221
|
5
|
2026-05-20T11:55:51.748772+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278151748_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
iilPhostormVIewINavigarecodeLaravelKeractorJOOISWi iilPhostormVIewINavigarecodeLaravelKeractorJOOISWindowFV faVsco.js#12098 on JY-20676-delete-report-related-ProiectC ActivityController.ong© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php XC) AutomatedReportsServicelest.onpm EventsAsKAnytningPromptservice.ong©ASKAnythingPromptDto.pngc Historyservice.ong© AskJiminnyReportsController.phg© AutomatedRe©) AskAnythingPromptServiceTest.php© Search.phpD AskJiminnyAiWAWS0 BillingManagementu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• EncryptionDES1M SakenD FeatureFlagsD FFMpegD FileSystemD Gong_ cuzzienutoC KeyPointsKIOSK_ LanquageDetectionW LOCKSW Math_Mediapioeline2 MobileSettinasIM ParagranhBreaker1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrocnorWorkdM Auoncclass ASkAnyth1ngPromptServicepublic function edit(if ($prompt->isDefaultPromptO) {$this->askAnythingRepository->hidePromptForUser($prompt, Suser);$newPrompt = $this->askAnythingRepository->createPrompt($user$prompt->getTargeto$titlescontent,ssharebroups.} elseif (Sprompt->qet0wnerIdO === Suser-›qet1dO) {$newPrompt = $this->askAnythingRepository->editPromptCSpromotScontent.?elsefthrow new InvalidArgumentException( message'Edit not allowed for prompt' . $prompt->getUu$newPrompt->getUuido.SnewPromnt->aetTitle0lSnewPnomnt->aetContentO$newPrompt->getTargetO$user->getUuido.Cchanollconcituidessharebroupsuulassnewrrompc->qechaskeporcsoAccept Reiectpublic function deletedAskAnvthingPromot Sprompt.User suserAskAnvthanaPromotnublic function reorder(User Suserarrav Soromotluids.)• void s..}1Accept File &+X Reject File +*€ + 1 of 4 files →=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]207208- -250251252253254© CoachingFeedbackCoachUserln.phpTx: AutovPlavaroundSo jiminny v021 A1 A18 V2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm contiqurations where 1d = 500:select * from users where name = "Integration Account"; # 1695SELE * FROMIsochal accounts WiERE soctable 10 = 16951select * from activities where crm confiauration 1d = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•select * from leads;SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'comoleted'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks tWHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESCLTMTT 25:select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive':SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idnomailsa.*,t.owner id FROM social accounts saOTM ucone non mid = ca cociablo idl• suppont Dally • In o m100% S2• Wed 20 May 14:55:51AutomatedReportsServiceTest ~CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..Note: The test name is also misleading - it tests updateAskJiminnyReport() (line 2877), not updateAskJiminnyReportStatuSO.3. Missing Success-Path Test for updateAskJiminnyReportStatus - NOT ADDEDcurrent Tests:•testUpdateAskJ1m1nnyReportstatusNotASKJ 1m1nny (misnamed, tests aitterent method)• testUodateAskJiminnvReportStatusThrowswhenEnabLinoWithMissinaReferences (error path)• testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling (disable path)Missing: Happy path where $status = true, isAsKJiminnyReport() = true, canExecute() = true → update() is calledand tresh is feturnedAedito Omits hacRenorts in DTO — NOT SIXEDILocation: AckAnvthinaPromntService.nbn lines 152-161=phpreturn new AskAnythingPromptDto(SnewPromnt-saetTitle().Tardetorsuser->getuuid),SchareGrounslluids// hasReports NOT PASSED → nullImnact: After an edit frontend receives no has renorts field causina the delete quard to he temnorarilv absent forFix: Pass Sprompt->getHasReports() (or false for newly created prompts in the default prompt branch) as the 8thparameter. matchina create" line 1045. fetchActivitySearch Silently Returns has reports: false - NOT FIXEDLocation: ActivityController.pho lines 1069-1088J UstActivitySearch (Uine 1100) - CORRECTISuser->searches ()->withCount('automatedReports')->get()return Schis→>response->wichltemSsearch. No withcount!View allnent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3anorte/M AutomatodDonortcGorvicoToct nhn 129-1ann/Comnonent/AskAnvthina/M AckAnvthinaPromntService.ohn t1lapo/Htto/Controllers/AP//[mActivitvController.ohn* Reiect alliiAccent allibut previouslv you mentioned that I don't have to add< CodeC° AdantiveW Windsurf Teamc1-1.UTE.8io 4 spaces...
|
NULL
|
2641172456954704060
|
NULL
|
click
|
ocr
|
NULL
|
iilPhostormVIewINavigarecodeLaravelKeractorJOOISWi iilPhostormVIewINavigarecodeLaravelKeractorJOOISWindowFV faVsco.js#12098 on JY-20676-delete-report-related-ProiectC ActivityController.ong© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.php XC) AutomatedReportsServicelest.onpm EventsAsKAnytningPromptservice.ong©ASKAnythingPromptDto.pngc Historyservice.ong© AskJiminnyReportsController.phg© AutomatedRe©) AskAnythingPromptServiceTest.php© Search.phpD AskJiminnyAiWAWS0 BillingManagementu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• EncryptionDES1M SakenD FeatureFlagsD FFMpegD FileSystemD Gong_ cuzzienutoC KeyPointsKIOSK_ LanquageDetectionW LOCKSW Math_Mediapioeline2 MobileSettinasIM ParagranhBreaker1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrocnorWorkdM Auoncclass ASkAnyth1ngPromptServicepublic function edit(if ($prompt->isDefaultPromptO) {$this->askAnythingRepository->hidePromptForUser($prompt, Suser);$newPrompt = $this->askAnythingRepository->createPrompt($user$prompt->getTargeto$titlescontent,ssharebroups.} elseif (Sprompt->qet0wnerIdO === Suser-›qet1dO) {$newPrompt = $this->askAnythingRepository->editPromptCSpromotScontent.?elsefthrow new InvalidArgumentException( message'Edit not allowed for prompt' . $prompt->getUu$newPrompt->getUuido.SnewPromnt->aetTitle0lSnewPnomnt->aetContentO$newPrompt->getTargetO$user->getUuido.Cchanollconcituidessharebroupsuulassnewrrompc->qechaskeporcsoAccept Reiectpublic function deletedAskAnvthingPromot Sprompt.User suserAskAnvthanaPromotnublic function reorder(User Suserarrav Soromotluids.)• void s..}1Accept File &+X Reject File +*€ + 1 of 4 files →=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]207208- -250251252253254© CoachingFeedbackCoachUserln.phpTx: AutovPlavaroundSo jiminny v021 A1 A18 V2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm contiqurations where 1d = 500:select * from users where name = "Integration Account"; # 1695SELE * FROMIsochal accounts WiERE soctable 10 = 16951select * from activities where crm confiauration 1d = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•select * from leads;SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'comoleted'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks tWHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESCLTMTT 25:select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive':SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idnomailsa.*,t.owner id FROM social accounts saOTM ucone non mid = ca cociablo idl• suppont Dally • In o m100% S2• Wed 20 May 14:55:51AutomatedReportsServiceTest ~CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..Note: The test name is also misleading - it tests updateAskJiminnyReport() (line 2877), not updateAskJiminnyReportStatuSO.3. Missing Success-Path Test for updateAskJiminnyReportStatus - NOT ADDEDcurrent Tests:•testUpdateAskJ1m1nnyReportstatusNotASKJ 1m1nny (misnamed, tests aitterent method)• testUodateAskJiminnvReportStatusThrowswhenEnabLinoWithMissinaReferences (error path)• testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling (disable path)Missing: Happy path where $status = true, isAsKJiminnyReport() = true, canExecute() = true → update() is calledand tresh is feturnedAedito Omits hacRenorts in DTO — NOT SIXEDILocation: AckAnvthinaPromntService.nbn lines 152-161=phpreturn new AskAnythingPromptDto(SnewPromnt-saetTitle().Tardetorsuser->getuuid),SchareGrounslluids// hasReports NOT PASSED → nullImnact: After an edit frontend receives no has renorts field causina the delete quard to he temnorarilv absent forFix: Pass Sprompt->getHasReports() (or false for newly created prompts in the default prompt branch) as the 8thparameter. matchina create" line 1045. fetchActivitySearch Silently Returns has reports: false - NOT FIXEDLocation: ActivityController.pho lines 1069-1088J UstActivitySearch (Uine 1100) - CORRECTISuser->searches ()->withCount('automatedReports')->get()return Schis→>response->wichltemSsearch. No withcount!View allnent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3anorte/M AutomatodDonortcGorvicoToct nhn 129-1ann/Comnonent/AskAnvthina/M AckAnvthinaPromntService.ohn t1lapo/Htto/Controllers/AP//[mActivitvController.ohn* Reiect alliiAccent allibut previouslv you mentioned that I don't have to add< CodeC° AdantiveW Windsurf Teamc1-1.UTE.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62674
|
2221
|
4
|
2026-05-20T11:55:32.876561+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278132876_m2.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
iilPhostormVIewINavigarecodeLaravelKeractorJOOISWi iilPhostormVIewINavigarecodeLaravelKeractorJOOISWindowFV faVsco.js#12098 on JY-20676-delete-report-related-olProiectC ActivityController.ong© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.phpC) AutomatedReportsServicelest.onpm EventsAsKAnytningPromptservice.ong©ASKAnythingPromptDto.pngc Historyservice.ongD AskJiminnyAiWAWSu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• EncryptionDES1M SakenD FeatureFlagsD FFMpegD FileSystemD Gong_ cuzzienutoC KeyPointsKIOSK_ LanquageDetectionW LOCKSW Math_Mediapioeline2 MobileSettinasIM ParagranhBreaker1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrocnorWorkdM Auonc© AskJiminnyReportsController.phg© AutomatedRe© AskAnythingPromptServiceTest.php© Search.php855$85class ASkAnyth1ngPromptServicepublic function edit(if ($prompt->isDefaultPromptO) {$this->askAnythingRepository->hidePromptForUser($prompt, Suser);$newPrompt = $this->askAnythingRepository->createPrompt($user$prompt->getTargeto$titlescontent,ssharebroups.} elseif (Sprompt->qet0wnerIdO === Suser-›qet1dO) {$newPrompt = $this->askAnythingRepository->editPromptCSpromotScontent.}else sthrow new InvalidArgumentException( message'Edit not allowed for prompt' . $prompt->getUu$newPrompt->getUuido.SnewPromnt->aetTitle0lSnewPnomnt->aetContentO$newPrompt->getTargetO$user->getUuido.Cchanollconcituidessharebroupsuulassnewrrompc->qechaskeporcsoAccept Reiectpublic function deletedAskAnvthingPromot Sprompt.User suserAskAnvthanaPromotnublic function reorder(User Suserarrav Soromotluids.D: void 4..1Accept File %+X Reject File 4%€ + 1 of 4 files →=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]207208- -250251253254© CoachingFeedbackCoachUserln.phpTx: AutovPlavaroundSo jiminny v021 A1 A18 V2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm contiqurations where 1d = 500:select * from users where name = "Integration Account"; # 1695SELE * FROMIsochal accounts WiERE soctable 10 = 16951select * from activities where crm confiauration 1d = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•select * from leads;SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'comoleted'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks tWHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESCLTMTT 25•select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive':SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idnomailsa.*,t.owner id FROM social accounts saOTM ucone non mid = ca cociablo idl• suppont Dally • In o m100% L2• Wed 20 May 14:55:32AutomatedReportsServiceTest ~CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..Note: The test name is also misleading - it tests updateAskJiminnyReport() (line 2877), not updateAskJiminnyReportStatuSO.3. Missing Success-Path Test for updateAskJiminnyReportStatus - NOT ADDEDcurrent Tests:•testUpdateAskJ1m1nnyReportstatusNotASKJ 1m1nny (misnamed, tests aitterent method)• testUodateAskJiminnvReportStatusThrowswhenEnabLinoWithMissinaReferences (error path)• testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling (disable path)Missing: Happy path where $status = true, isAsKJiminnyReport() = true, canExecute() = true → update() is calledand tresh is feturnedAedito Omits hacRenorts in DTO — NOT SIXEDILocation: AckAnvthinaPromntService.nbn lines 152-161=phpreturn new AskAnythingPromptDto(SnewPromnt-saetTitle().Tardetorsuser->getuuid),SchareGrounslluids// hasReports NOT PASSED → nullImnact: After an edit frontend receives no has renorts field causina the delete quard to he temnorarilv absent forFix: Pass Sprompt->getHasReports() (or false for newly created prompts in the default prompt branch) as the 8thparameter. matchina create" line 1045. fetchActivitySearch Silently Returns has reports: false - NOT FIXEDLocation: ActivityController.pho lines 1069-1088J UstActivitySearch (Uine 1100) - CORRECTISuser->searches ()->withCount('automatedReports')->get()return schis→>response->w1chltemSsearch. No withcount!View allinent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3norte/@ AutomatodDonortcGorvicoToct nhn 129-1a/MAskAnvthinaPromntService.ohn t1apo/Htto/Controllers/AP|/[m ActivitvController.ohv* Reiect alliiAccent alliAsk anvthing 884-D)< CodeC° AdantiveW Windsurf Teamc1-1.UTE.8Po 4 spaces...
|
NULL
|
-3608924345793696254
|
NULL
|
click
|
ocr
|
NULL
|
iilPhostormVIewINavigarecodeLaravelKeractorJOOISWi iilPhostormVIewINavigarecodeLaravelKeractorJOOISWindowFV faVsco.js#12098 on JY-20676-delete-report-related-olProiectC ActivityController.ong© AskAnythingController.php© AskAnythingPromptDto.php© AskAnythingPromptService.phpC) AutomatedReportsServicelest.onpm EventsAsKAnytningPromptservice.ong©ASKAnythingPromptDto.pngc Historyservice.ongD AskJiminnyAiWAWSu cachew countryDatabase→ DatadogDatettimeDealinsiahtsN DealRisks1N GlasticSearchM EloquentEncoding• EncryptionDES1M SakenD FeatureFlagsD FFMpegD FileSystemD Gong_ cuzzienutoC KeyPointsKIOSK_ LanquageDetectionW LOCKSW Math_Mediapioeline2 MobileSettinasIM ParagranhBreaker1 PartitionedCookieM PlavbackPadeM PlavlistProphetM PronhetAfM DrocnorWorkdM Auonc© AskJiminnyReportsController.phg© AutomatedRe© AskAnythingPromptServiceTest.php© Search.php855$85class ASkAnyth1ngPromptServicepublic function edit(if ($prompt->isDefaultPromptO) {$this->askAnythingRepository->hidePromptForUser($prompt, Suser);$newPrompt = $this->askAnythingRepository->createPrompt($user$prompt->getTargeto$titlescontent,ssharebroups.} elseif (Sprompt->qet0wnerIdO === Suser-›qet1dO) {$newPrompt = $this->askAnythingRepository->editPromptCSpromotScontent.}else sthrow new InvalidArgumentException( message'Edit not allowed for prompt' . $prompt->getUu$newPrompt->getUuido.SnewPromnt->aetTitle0lSnewPnomnt->aetContentO$newPrompt->getTargetO$user->getUuido.Cchanollconcituidessharebroupsuulassnewrrompc->qechaskeporcsoAccept Reiectpublic function deletedAskAnvthingPromot Sprompt.User suserAskAnvthanaPromotnublic function reorder(User Suserarrav Soromotluids.D: void 4..1Accept File %+X Reject File 4%€ + 1 of 4 files →=custom.log= laravel.log4 SF jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]207208- -250251253254© CoachingFeedbackCoachUserln.phpTx: AutovPlavaroundSo jiminny v021 A1 A18 V2 V6 ^SELECT * FROM automated reports where id = 67:SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143:select * from crm contiqurations where 1d = 500:select * from users where name = "Integration Account"; # 1695SELE * FROMIsochal accounts WiERE soctable 10 = 16951select * from activities where crm confiauration 1d = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SSIEe * SROM activities WHERS uurd to binf:458c/015-h914-4000-h083-5687h32620569 = Wnd•select * from leads;SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003);SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'comoleted'AND uuid to bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuidAND a.deleted at Is NuLlAND EXISTS(SELECT 1 FROM tracks tWHERE t.activity_id = a.idAND t.type IN ('audio', 'video')ORDER RY a.actual stant time DESCLTMTT 25•select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive':SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idnomailsa.*,t.owner id FROM social accounts saOTM ucone non mid = ca cociablo idl• suppont Dally • In o m100% L2• Wed 20 May 14:55:32AutomatedReportsServiceTest ~CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+0 ..Note: The test name is also misleading - it tests updateAskJiminnyReport() (line 2877), not updateAskJiminnyReportStatuSO.3. Missing Success-Path Test for updateAskJiminnyReportStatus - NOT ADDEDcurrent Tests:•testUpdateAskJ1m1nnyReportstatusNotASKJ 1m1nny (misnamed, tests aitterent method)• testUodateAskJiminnvReportStatusThrowswhenEnabLinoWithMissinaReferences (error path)• testUpdateAskJiminnyReportStatusDoesNotValidateWhenDisabling (disable path)Missing: Happy path where $status = true, isAsKJiminnyReport() = true, canExecute() = true → update() is calledand tresh is feturnedAedito Omits hacRenorts in DTO — NOT SIXEDILocation: AckAnvthinaPromntService.nbn lines 152-161=phpreturn new AskAnythingPromptDto(SnewPromnt-saetTitle().Tardetorsuser->getuuid),SchareGrounslluids// hasReports NOT PASSED → nullImnact: After an edit frontend receives no has renorts field causina the delete quard to he temnorarilv absent forFix: Pass Sprompt->getHasReports() (or false for newly created prompts in the default prompt branch) as the 8thparameter. matchina create" line 1045. fetchActivitySearch Silently Returns has reports: false - NOT FIXEDLocation: ActivityController.pho lines 1069-1088J UstActivitySearch (Uine 1100) - CORRECTISuser->searches ()->withCount('automatedReports')->get()return schis→>response->w1chltemSsearch. No withcount!View allinent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3norte/@ AutomatodDonortcGorvicoToct nhn 129-1a/MAskAnvthinaPromntService.ohn t1apo/Htto/Controllers/AP|/[m ActivitvController.ohv* Reiect alliiAccent alliAsk anvthing 884-D)< CodeC° AdantiveW Windsurf Teamc1-1.UTE.8Po 4 spaces...
|
62672
|
NULL
|
NULL
|
NULL
|
|
62673
|
2220
|
3
|
2026-05-20T11:55:32.873294+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278132873_m1.jpg...
|
PhpStorm
|
faVsco.js – AskAnythingPromptService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 5 mA100% <78• Wed 20 May 14:55:32screenpipe"T₴1|84DOCKER• 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
7872174727862931349
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 5 mA100% <78• Wed 20 May 14:55:32screenpipe"T₴1|84DOCKER• 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
62671
|
NULL
|
NULL
|
NULL
|
|
62672
|
2221
|
3
|
2026-05-20T11:55:09.826695+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278109826_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62671
|
2220
|
2
|
2026-05-20T11:55:09.798995+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278109798_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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":"AutomatedReportsServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AutomatedReportsServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
62670
|
2221
|
2
|
2026-05-20T11:55:04.643528+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278104643_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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}]...
|
-5524660161020980253
|
2674298500603969
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
PhostormVIewINavigarecodeKeractorJOOISWindowmelpFV faVsco.js#12098 on JY-20676-delete-report-relatedProiect v© ActivityController.phpphp api.php© AskAnythingController.php© AskAnythingPromptService.phpAsKAnytIngkepository.ong© AutomatedReportsServiceTest.phpAutomatedReports_ calendarsD Crm© AskAnythingPromptDto.php© AskJiminnyReportsController.phpu clasticsearch_ GroupsC Import_NudaesD OpportunitiesPlaybooksSidekickMusers97311865C Event. oho© EventDispatcher.ohp• M ExcentionsD FFMpegIM FormatsO Guards• M Helnerc0 Http1M AccoccTokenProviderD ControllersVMAD10761071107210731075MAiCallScoring1076_ AKeports1072> CJ DeallnsightsOoporunity› 0 Page1079• Scorecards›D Settinas• C TeamInsights›D Themes• _ UserAutomatedRevorts© UserAutomatedReportsController.$ 1086v Mv2(C) ActivitvV2Controller.oho@ AskAnvthinaController.ohvC) Ask.JiminnvReoortsControlier.onoC) DealsV2Controller.oho10901991© AutomatedReportsService.php) AskAnythingPromptServiceTest.phptinal class ActivitycontrollerYrwwTw(C) OnDemandV2Controller.nhn©PlaylistController.phpC) PlavlistShareController.ohr% PlavlistTrackController.php11A8© TranscriptionSummaryController.pl 11ec© UploadController.php1116© ActionitemsController.phpC0 ActivitvControllor nhr1992 1>© AiCrmNotesController.php© BaseController.phpT usageprivate function storeNamedSearchFilters(colleccion srequesc,search ssearch,FilterDefinitionCollection SfilterSet?string Sprefix = nulu)• self {...}* achrows AuthorzzationExcentionGET lapi/v1/activitv/saved-search/[search}laoi.savedsearch.showioublic function fetchActivitvSearchdSearch Ssearch.Reauest SreauestSpanchtinansformen Scparchtinansfarmen): JsonResponse 1Sthis->authonized ability "viewl Sceanch)•/** @var User $user */$user = Srequest->userO:cthie-snocnonco->getManagerO->setSerializer(new JsonSerializer0):Ssearch->LoadCount( relations:'automatedReports'):return sth1s->response->w1thltemo->wathconsumer(Suser)* Neletes a saved cearch* Qnaram Reaues+ SreauestAccept File &+X Reject File 4%€+ 3 of 4 files →* Athnowe Eycontior© Search.phpAccept Reject=custom.log= laravel.log4 SF [jiminny@localhost] x4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING)207208=2091210211212213214- — 236237238239240—241242248© CoachingFeedbackCoachUserln.phpTx: AutovriaygroundvSo jiminny v021 A1 A18 V2 Y6 л VSELECT * FROM automated_reports where id = 67;SELECT * FROM automated reports where id = 42:SELECT * FROM Users WHERE id = 143: # group 28select * from teams where id = 3143;select * from crm_configurations where id = 500:select * from users where name = 'Integration Account'; # 1695SELECT * FROMsocial accounts WHERE sociable id = 1695:select * from activities where crm confiquration id = 39and recording_state = 'recorded' and duration > 60and status ='comoleted' and actual start time >=12025-12-011SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;splect * from leads.SELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = vuid; # 422003SELECT * FROM activities WHERE id IN (16,422003):SELECT * FROM activities where status = 'failed':SELECT * FROM tracks WHERE activity_id = 422003;SELECTFROM activities aJOIN users u 1..n<->1: ON a.user id = u.idWHEREa.status = 'completed'AND uvid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uvidAND a.deleted at IS NULISELECT 1 FROM tracks tNHERE tactivitv id = a.idAND t.tvoe IN ('audio', "video')ORDER RY a.actual stant time DESdLIMIT 25;select * from teams where id = 19;select * from crm_configurations where provider = 'pipedrive';CONCATEN #d CASE WHEN n id - + ounon #d THEN 1 (ounon)! EISE 11 ENO) AS ncon ¿dl+ ounon id EPOM cocial Accounte caOTM ucone mon mid = ca cociablo id• suppont Dally • In om100% L2Wed 20 May 14:55:04CascadeTesting SmsReceivedFix UserinvitationdteReviewing ASKAnythin+O •ActivityController.phpThouahtsNow I need to add the missing success-path test for uodateAskJiminnvReportStatus :Read AutomatedRenortsServiceTest.oho #L2915-2934"AutomatedReportsServiceTest.ohAll 5 outstandina issues from the code review have been imolemented:Summary of Changes1. Test Fixes - $this->any() to Exact CountsFile: AskAnythingPromptServiceTest.php• Line 336: Sthis->any() → Sthis->exactly(3) for Sprompt->getId() in testDeleteAskAnythingPrompt• Line 382: Sthis-sanv → Sthis-sexactly(3) for Soromot->qetId@ in testDeleteAskAnvthingPromotWithRelatio• Line 389: Sthis-sanv() → sthis-sonce() for Suser->getIdO in testDeleteAskAnvthingPromotwithRelations2. Test Fix - Bare method() to expects()File: AutomatedReportsservicelest.pho• Line 2801: SmockReport->method(1SASKJiminnyReport) → SmockReport→>expects(Sthis→>once))->method(1SASKJimInnyReport")3. New Test - Success Path for updateAskJiminnyReportStatusFile: AutomatedReportsServiceTest.pho• Added testUpdateAskJiminnyReportStatusSuccesstesting.•sstatus = true. isAskJiminnyReport() = true, canExecute() = true• Verifies undate() called with correct params•Verifies fresh() returned4. Bua FiX — edito Missing hasReports in DTOFile: AskAnvthinaPromotService.oho• Added SnewPromot→>qetHasReports() as 8th parameter to AskAnvthinaPromotDto constructor in edito method5. Rua Fix — fetchActivitvSearch Miccina autonatedRenorts CountFile: Activitycontroller.phg•Added ssearch->loadCount('automatedReports') before returning response in fetchActivitySearch() methodResult: All critical functional bugs and test standards violations from the code review are now resolved.nent/AskAnything/© AskAnythingPromptServiceTest.php +3 -3norte/@ AutomatodDonortcGorvicoToct nhn 129-1a/MAskAnvthinaPromntService.ohn t1apo/Htto/Controllers/AP|/[m ActivitvController.ohv* Reiect alliiAccent alliAsk anvthing 884-D)< CodeC AdantiveW Windsurf Teamc1-1.UTE.8io 4 spaces...
|
62668
|
NULL
|
NULL
|
NULL
|
|
62669
|
2220
|
1
|
2026-05-20T11:55:04.583159+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278104583_m1.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 5 mA100% С8• Wed 20 May 14:55:04T₴1|screenpipe"O ₴4DOCKERO 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
NULL
|
-1734907854775568062
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(aolAPP (-zsh)₴2Support Daily - in 5 mA100% С8• Wed 20 May 14:55:04T₴1|screenpipe"O ₴4DOCKERO 81tests/Unit/Policies/CanAccessAiReportsTest.phpDEV (-zsh)APP (-zsh)nochanges added to commit (use "git add"and/or"git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny~/jiminny/app (JY-20676-delete-report-related-objects) $ git pullremote:Enumerating objects: 157,done.remote: Counting objects: 100% (157/157),done.remote: Compressing objects: 100% (42/42),done.remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s,done.Resolving deltas: 100% (120/120), completed with 37 local objects.From github.com:jiminny/app3f5f9b21c7..202ab55be7JY-20676-delete-report-related-objects-> origin/JY-20676-delete-report-related-objects3642c9384c..cf70da243aJY-20613-allow-owner-role-on-team-setup ->* [new branch]origin/JY-20613-allow-owner-role-on-team-setup* [new branch]JY-20808-schedule-reset-on-low-queue-> origin/JY-20808-schedule-reset-on-low-queueJY-20930-ringcentral-disconnected-crma873f0cbce..71b36decb6origin/JY-20930-ringcentral-disconnected-crmmaster-> origin/masterUpdating 56ca84fabd..202ab55be7Fast-forwardapp/Component/ES/AsyncUpdateElasticSearch.php++++app/Component/ES/Listeners/UpdateMultipleTargetsListener.phpapp/Component/ES/Listeners/UpdateSingleTargetListener.phpapp/Component/ES/Processor/Actions/UpsertDocumentsAction.phpapp/Component/ES/Processor/DT0s/SelectionList.phpapp/Component/ES/Processor/TargetEntitiesSelector.phpapp/Component/ES/Processor/Traits/SelectEntityListTrait.phpapp/Component/ES/QueuePriorityEnum.phpapp/Component/ES/UpdateProcessManager.phpapp/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.phpapp/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.phpapp/Services/RecallAI/Commands/ScheduleBotCommand.php27310+++++-37147314021126app/Services/RecallA1/RecallAIService.phpfront-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.jsfront-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.jsfront-end/src/components/shared/SavedSearchModal/useSavedSearch.jstests/Unit/Component/ES/AsyncUpdateElasticSearchTest.phptests/Unit/Component/ES/Processor/BatchStatusManagerTest.phptests/Unit/Component/ES/Processor/DT0s/SelectionListTest.phptests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.phptests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.phptests/Unit/Component/ES/QueuePriorityEnumTest.phptests/Unit/Component/ES/UpdateProcessManagerTest.phptests/Unit/Services/RecallAI/RecallAIServiceTest.php16055+++++[PHONE]04424 files changed, 619 insertions(+), 196 deletions(-)create mode 100644 app/Component/ES/QueuePriorityEnum.phpcreate mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.phpAPP...
|
62666
|
NULL
|
NULL
|
NULL
|
|
62668
|
2221
|
1
|
2026-05-20T11:55:02.054045+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-20/1779 /Users/lukas/.screenpipe/data/data/2026-05-20/1779278102054_m2.jpg...
|
PhpStorm
|
faVsco.js – ActivityController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
[{"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":"#12098 on JY-20676-delete-report-related-objects, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.119015954,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12098 exists for current branch JY-20676-delete-report-related-objects","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.8218085,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AutomatedReportsServiceTest","depth":6,"bounds":{"left":0.83710104,"top":0.019952115,"width":0.078457445,"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 'AutomatedReportsServiceTest'","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 'AutomatedReportsServiceTest'","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\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\API;\n\nuse Carbon\\Carbon;\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Validation\\Rule;\nuse Illuminate\\Validation\\Rules\\In;\nuse Illuminate\\Validation\\ValidationException;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ActivityAnalytics;\nuse Jiminny\\Component\\ActivitySearch;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Contracts\\Nudge\\NudgeFactoryInterface;\nuse Jiminny\\Contracts\\Playlist\\PlaylistTrackFactoryInterface;\nuse Jiminny\\Contracts\\Repositories\\PlaylistActivityRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Enums\\TeamSetting;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Events\\Activities\\Coaching\\Coached;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Http\\Controllers\\API\\BaseController as Controller;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Responses\\Api\\AbstractResponse;\nuse Jiminny\\Http\\Responses\\Api\\Response;\nuse Jiminny\\Http\\Serializers\\JsonSerializer;\nuse Jiminny\\Http\\Transformers\\ActivityCommentTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTopicTriggerTransformer;\nuse Jiminny\\Http\\Transformers\\ActivityTransformer;\nuse Jiminny\\Http\\Transformers\\AvailabilityNotificationTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingFeedbackTransformer;\nuse Jiminny\\Http\\Transformers\\CoachingSectionsTransformer;\nuse Jiminny\\Http\\Transformers\\SearchTransformer;\nuse Jiminny\\Http\\Transformers\\StatsTransformer;\nuse Jiminny\\Jobs\\Crm\\SaveActivity;\nuse Jiminny\\Jobs\\Crm\\UpdateStage;\nuse Jiminny\\Jobs\\Telephony\\StartRecording;\nuse Jiminny\\Jobs\\Telephony\\StopRecording;\nuse Jiminny\\Jobs\\Telephony\\ToggleRecording;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\CoachingSectionCriterion;\nuse Jiminny\\Models\\CoachingSectionFeedback;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\LayoutEntity;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\LanguageDialect;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\CoachingFeedbackRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Rules\\CrmReference;\nuse Jiminny\\Rules\\MultidimensionalArrayMaxCharRule;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\PlaybackService;\nuse Jiminny\\Services\\UserService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry;\nuse Symfony\\Component\\HttpFoundation;\n\nfinal class ActivityController extends Controller implements CommentContextInterface\n{\n // Number of minutes to look back on activities. i.e. a timeout on activity duration.\n private const int LOOK_BACK = 180;\n\n public function __construct(\n private ProviderRegistry $providerRegistry,\n private ActivityService $activityService,\n Response $response,\n private UserService $userService,\n private ActivitySearch\\Service\\ActivitySearch $activitySearch,\n private NudgeFactoryInterface $nudgeFactory,\n private ActivityCommentService $activityCommentService,\n private LoggerInterface $logger,\n private readonly CoachingFeedbackRepository $coachingFeedbackRepository,\n private readonly TeamRepository $teamRepository,\n ) {\n parent::__construct($response);\n }\n\n public static function getCommentImplementation(): string\n {\n return Comment::class;\n }\n\n public function delete()\n {\n $this->request->validate([\n '*' => 'uuid:activities',\n ]);\n\n $deletedIds = [];\n foreach ($this->request->all() as $activityId) {\n $activity = Activity::idOrUuId($activityId);\n\n try {\n if ($this->authorize('delete', $activity)) {\n $activity->delete();\n $deletedIds[] = $activityId;\n\n \\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n }\n } catch (AuthorizationException $authorizationException) {\n // They didn't have permission.\n }\n }\n\n return $this->response->withArray($deletedIds);\n }\n\n public function update(Request $request, Activity $activity)\n {\n $this->authorize('updateMetadata', $activity);\n\n $request->validate([\n 'title' => 'string|max:250',\n 'category_id' => 'uuid:playbook_categories',\n 'language' => [\n new In(\n LanguageDialect::query()\n ->with('language')\n ->cursor()\n ->map(static function (LanguageDialect $languageDialect): string {\n return $languageDialect->getLanguageLocale();\n })\n ->all()\n ),\n ],\n ]);\n\n if ($request->has('title')) {\n $activity->title = $request->input('title');\n }\n\n if ($request->has('category_id')) {\n $category = PlaybookCategory::uuid($request->input('category_id'));\n\n if ($category->playbook->team_id !== $request->user()->team_id) {\n return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n if ($request->has('language')) {\n if (! $activity->isInProgress()) {\n return $this->response->withError(\n 'Activity language can only be set while the meeting is in progress.',\n 400\n );\n }\n\n $activity->setLanguageCode($request->input('language'));\n }\n\n $activity->save();\n\n return $this->response->withOk();\n }\n\n // XXX: This should be merged with the update method.\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws SocialAccountTokenInvalidException\n *\n * @return mixed\n */\n public function summarize(Activity $activity): mixed\n {\n $this->logger->info('[Log Activity] Summarizing activity ', [\n 'activityId' => $activity->getUuid(),\n 'payload' => $this->request->all(),\n ]);\n $this->authorize('update', $activity);\n\n $this->logger->info('[Log Activity] Validating summary');\n // Validate the payload.\n $this->validateSummary($activity);\n\n // All objects must belong to this team.\n /** @var User $user */\n $user = $this->request->user();\n $team = $user->getTeam();\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n try {\n $crmUser = $user;\n if ($user->isCrmRequired() === false) {\n $crmUser = $team->owner;\n }\n $crmService->setUser($crmUser);\n } catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());\n }\n\n $rawEntities = $this->request->input('entities');\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid(\n $this->request->input('layout_id')\n );\n\n // Delay execution of CRM jobs to avoid locking issues.\n $jobDelay = 0;\n\n // If we have arrived from a notification, mark it as read.\n $notificationId = $this->request->input('nId');\n if ($notificationId) {\n $notification = $user->unreadNotifications->where('id', $notificationId)->first();\n\n if ($notification) {\n $notification->markAsRead();\n }\n }\n\n $title = $this->request->input('title');\n $prospects = $this->request->input('prospects');\n $opportunityId = $this->request->input('opportunity_id');\n $stageId = $this->request->input('stage_id');\n $categoryId = $this->request->input('category_id');\n $summary = $this->request->input('summary');\n $crmProviderId = $this->request->input('crm_id');\n $isInternal = $this->request->input('is_internal') ?? false;\n\n $lead = null;\n $category = null;\n $account = null;\n $contact = null;\n $opportunity = null;\n $stage = null;\n $callStage = null;\n\n foreach ($prospects as $prospectData) {\n $objectId = $prospectData['id'];\n\n if ($objectId === null) {\n continue;\n }\n\n $objectType = $prospectData['type'];\n $this->logger->info('debug', ['prospect_data' => $prospectData]);\n\n try {\n if ($objectType === null) {\n $this->logger->info('no object type');\n if ($crmService instanceof SupportsObjectTypeParseInterface) {\n $objectType = $crmService->parseObjectType($objectId);\n }\n }\n\n switch ($objectType) {\n case 'lead':\n $this->logger->info('Processing lead');\n /** @var Lead|null $lead */\n $lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();\n\n // Lead does not exist locally, import it.\n if ($lead === null) {\n $this->logger->info('Lead does not exist locally');\n /** @var Lead $lead */\n $lead = $crmService->syncLead($objectId);\n }\n\n $this->logger->info('Lead found', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n if ($stageId === null) {\n $this->logger->info('Stage ID is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $lead->stage;\n\n break;\n }\n\n $this->logger->info('Looking for stage');\n // Determine if they have changed the stage.\n /** @var Stage $stage */\n $stage = $team->crm->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_LEAD)\n ->firstOrFail();\n\n $this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);\n if ($lead->stage_id && $lead->stage_id !== $stage->id) {\n $this->logger->info('Stage has changed');\n // Storage current stage on activity.\n $callStage = $lead->stage;\n\n // The stage has changed, update in remote CRM.\n dispatch(new UpdateStage($activity, $lead, $callStage, $stage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing lead stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->getName(),\n $stage->getName()\n ),\n [\n 'user' => $user->getUuid(),\n 'lead' => $lead->getUuid(),\n ]\n );\n } else {\n $this->logger->info('Stage has not changed');\n // Stage remains as current.\n $callStage = $stage;\n }\n\n break;\n\n case 'account':\n $this->logger->info('Processing account');\n // If the object is not a lead, it should be an account.\n $account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();\n\n // Account does not exist locally, import it.\n if ($account === null) {\n $this->logger->info('Account does not exist locally');\n $account = $crmService->syncAccount($objectId);\n }\n\n $this->logger->info('Account found', ['accountId' => $account->id]);\n\n break;\n case 'contact':\n $this->logger->info('processing contact');\n $contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();\n\n // Contact does not exist locally, import it.\n if (! $contact instanceof Contact) {\n $this->logger->info('contact does not exist locally');\n $contact = $crmService->syncContact($objectId);\n }\n\n $this->logger->info('resolving account');\n $account = $this->resolveAccount($team, $contact, $crmService, $prospects);\n\n break;\n }\n\n // If they have specified an opportunity, retrieve this with stage.\n if ($opportunityId) {\n $this->logger->info('opportunity id is set');\n $opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();\n\n // Opportunity does not exist locally, import it.\n if ($opportunity === null) {\n $this->logger->info('opportunity does not exist locally');\n $opportunity = $crmService->syncOpportunity($opportunityId);\n }\n\n if ($stageId === null) {\n $this->logger->info('stage id is null');\n // If it was not provided, just assume it is the current stage.\n $callStage = $opportunity->stage ?? null;\n } else {\n $this->logger->info('looking for stage');\n /** @var ?Stage $opportunityStage */\n $opportunityStage = $team->crm\n ->stages()\n ->uuid($stageId, false)\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n // There is a chance we still cannot import this opportunity.\n if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {\n $this->logger->info('opportunity stage has changed');\n // Storage current stage on activity.\n $callStage = $opportunity->stage;\n\n dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));\n\n $this->logger->info(\n sprintf(\n '[%s] User changing opportunity stage from %s to %s',\n $crmService->getDisplayName(),\n $callStage->name,\n $opportunityStage->name\n ),\n [\n 'userId' => $user->id_string,\n 'opportunityId' => $opportunity->id_string,\n ]\n );\n } else {\n $this->logger->info('opportunity stage has not changed');\n // Stage remains as current.\n $callStage = $opportunityStage;\n }\n }\n }\n\n if ($crmProviderId) {\n // Cast $crmProviderId to string otherwise it won't use database index for some records\n $linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();\n\n // Check if this activity has already been assigned to a different activity.\n if ($linkedActivity && $linkedActivity->id !== $activity->id) {\n throw new InvalidArgumentException(\n 'Sorry, the linked task has already been logged under a different call. '\n . 'Please choose another linked task.'\n );\n }\n }\n } catch (InvalidArgumentException $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorWrongArgs($exception->getMessage());\n } catch (Exception $exception) {\n $this->logger->error('Failed to process prospect', [\n 'prospect_data' => $prospectData,\n 'reason' => $exception->getMessage(),\n ]);\n\n // Return a JSON response with the response array and status code.\n return $this->response->errorInternalError(\n 'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'\n );\n }\n }\n\n if ($categoryId) {\n $category = PlaybookCategory::uuid($categoryId);\n\n if ($category->playbook->team_id !== $team->id) {\n throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');\n }\n\n $activity->playbook_category_id = $category->id;\n }\n\n $this->logger->info('Prospect data', [\n 'lead_id' => $lead?->getId(),\n 'account_id' => $account?->getId(),\n 'contact_id' => $contact?->getId(),\n 'opportunity_id' => $opportunity?->getId(),\n 'stage_id' => $stage?->getId(),\n ]);\n\n if ($title) {\n $activity->title = $title;\n }\n\n if ($summary) {\n $activity->summary = $summary;\n }\n\n if ($crmProviderId) {\n $activity->crm_provider_id = $crmProviderId;\n }\n\n if ($callStage) {\n $this->logger->info('Setting stage id', ['stageId' => $callStage->id]);\n $activity->stage_id = $callStage->id;\n }\n\n if ($lead) {\n $this->logger->info('Setting lead id', ['leadId' => $lead->id]);\n $activity->lead_id = $lead->id;\n\n // If we are changed from an account > lead, unset the account data.\n $this->logger->info('Unsetting account id, opportunity id, contact id, value');\n $activity->account_id = null;\n $activity->opportunity_id = null;\n $activity->contact_id = null;\n $activity->value = null;\n }\n\n if ($account) {\n $this->logger->info('Setting account id', ['accountId' => $account->id]);\n $activity->account_id = $account->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('unsetting lead id');\n $activity->lead_id = null;\n\n // Unset the contact if switching different accounts. Will be set up below if still applicable.\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {\n $this->logger->info('Unsetting contact id');\n $activity->contact_id = null;\n }\n }\n\n if ($opportunity) {\n $this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);\n $this->logger->info('unsetting lead id');\n $activity->opportunity_id = $opportunity->id;\n $activity->value = $opportunity->value;\n\n // If we are changed from an lead > account, unset the lead data.\n $activity->lead_id = null;\n }\n\n if ($contact) {\n $this->logger->info('setting contact id', ['contactId' => $contact->id]);\n $activity->contact_id = $contact->id;\n\n // If we are changed from an lead > account, unset the lead data.\n $this->logger->info('Unsetting lead id');\n $activity->lead_id = null;\n }\n\n $activity->is_internal = $isInternal;\n $activity->save();\n $activity->refresh();\n\n $this->logger->notice('Activity saved', [\n 'activity_id' => $activity->getId(),\n 'lead_id' => $activity->lead_id,\n 'account_id' => $activity->account_id,\n 'contact_id' => $activity->contact_id,\n 'opportunity_id' => $activity->opportunity_id,\n 'stage_id' => $activity->stage_id,\n 'crm_provider_id' => $activity->getCrmProviderId(),\n ]);\n\n // Store entities as field data on the activity.\n $updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);\n\n if ($activity->isLoggable()) {\n // Follow-up Task or Event data.\n $followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);\n\n $this->logger->info('CRM LOG manual log triggered', [\n 'activityId' => $activity->getUuid(),\n 'followupData' => $followupData,\n 'userId' => $user->getUuid(),\n ]);\n\n // Store data in the CRM.\n // ++add check for crm_required\n $job = new SaveActivity($activity, $followupData);\n\n if ($updatedData) {\n $job->delay(Carbon::now()->addMinutes($jobDelay));\n }\n\n dispatch($job);\n\n // Manually dispatch log for Opportunity or Prospect added\n if ($activity->hasOpportunity() || $activity->hasProspect()) {\n event(new ActivityProspectAdded(\n activity: $activity,\n eventSource: 'manually-log-crm-data'\n ));\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.\n *\n * @param ServiceInterface $service\n * @param Activity $activity\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array\n {\n $updatedData = [];\n $existingData = $activity->data()->get();\n\n // We need to delete any existing data to overwrite with latest values.\n $activity->data()->delete();\n\n $layoutEntities = $layout->entities()\n ->with('field', 'parent')\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->get();\n\n /** @var LayoutEntity $entity */\n foreach ($layoutEntities as $entity) {\n // If the user has provided a value for this entity\n if (array_key_exists($entity->id_string, $entities)) {\n $value = $entities[$entity->id_string];\n\n // Convert raw data into values that the CRM can consume.\n if ($value) {\n $value = $service->normalizeValue($entity->field->type, $value);\n }\n\n // Check the field is part of the activity-summary section.\n if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {\n // This is the internal database ID, not the external CRM ID.\n $objectId = null;\n\n switch ($entity->field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $objectId = $activity->account_id;\n\n break;\n\n case Field::OBJECT_CONTACT:\n $objectId = $activity->contact_id;\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n $objectId = $activity->opportunity_id;\n\n break;\n\n case Field::OBJECT_LEAD:\n $objectId = $activity->lead_id;\n\n break;\n\n case Field::OBJECT_TASK:\n case Field::OBJECT_EVENT:\n $objectId = $activity->id;\n\n break;\n }\n\n if ($objectId) {\n /** @var FieldData $data */\n $data = $activity->data()->create([\n 'crm_layout_entity_id' => $entity->id,\n 'crm_field_id' => $entity->crm_field_id,\n 'object_type' => $entity->field->object_type,\n 'object_id' => $objectId,\n 'value' => $value,\n ]);\n\n // Never send read-only field data to the CRM.\n if ($entity->read_only === false && $entity->is_visible) {\n $existingValue = $existingData\n ->where('crm_layout_entity_id', $entity->id)\n ->where('crm_field_id', $entity->crm_field_id)\n ->where('object_type', $entity->field->object_type)\n ->where('object_id', $objectId)\n ->first();\n\n // If the field was actually changed, we need to reflect this in the CRM too.\n if ($existingValue === null || $existingValue->value !== $value) {\n $updatedData[] = $data->id;\n }\n }\n }\n }\n }\n }\n\n return $updatedData;\n }\n\n /**\n * Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.\n *\n * @param ServiceInterface $crmService\n * @param Layout $layout\n * @param array $entities The raw entity data from user\n *\n * @return array\n */\n private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array\n {\n $fieldData = [];\n foreach ($entities as $entityId => $value) {\n // Only bother with fields that have a value.\n if ($value) {\n // Extract the entity from the UUID. Check the field is valid and part of the follow-up section.\n $entity = $layout->entities()\n ->uuid($entityId, false)\n ->whereHas('parent', function ($query) {\n $query->where('label', 'follow-up');\n })\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->first();\n\n if ($entity) {\n // Convert raw data into values that the CRM can consume.\n $value = $crmService->normalizeValue($entity->field->type, $value);\n\n // Add the field and value to the payload.\n $fieldData += [\n $entity->field->crm_provider_id => $value,\n ];\n }\n }\n }\n\n return $fieldData;\n }\n\n /**\n * @param Activity $activity\n */\n private function validateSummary(Activity $activity): void\n {\n $team = $activity->user->team;\n $crmProvider = $team->crm->provider;\n $attributes = [];\n\n $rules = [\n 'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,\n 'title' => 'string|max:250',\n 'prospects' => 'required|array',\n 'opportunity_id' => new CrmReference($crmProvider),\n 'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',\n 'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator\n 'summary' => 'max:50000',\n 'nId' => 'exists:notifications,id',\n 'crm_id' => new CrmReference($crmProvider),\n 'entities' => 'array',\n 'is_internal' => 'boolean',\n ];\n\n /** @var Layout $layout */\n $layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));\n\n // Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.\n $entities = $layout->entities()\n ->where('read_only', 0)\n ->whereHas('field', function ($query) {\n $query->where('is_selectable', 1);\n })\n ->whereHas('parent', function ($query) use ($activity) {\n if ($activity->isLoggable() === false) {\n $query->where('label', '<>', 'follow-up');\n }\n });\n\n $isInternal = $this->request->input('is_internal', false);\n\n foreach ($entities->get() as $entity) {\n $rules += $this->buildFieldValidator($entity, $isInternal);\n $attributes += $this->buildFieldMessage($entity);\n }\n\n $this->request->validate($rules, [], $attributes);\n }\n\n private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array\n {\n return [\n 'entities.' . $entity->id_string => $entity->getValidator($isInternal),\n ];\n }\n\n /**\n * @param LayoutEntity $entity\n *\n * @return array\n */\n private function buildFieldMessage(LayoutEntity $entity): array\n {\n $label = $entity->label;\n if ($label === null) {\n $label = $entity->field->label;\n }\n\n return [\n 'entities.' . $entity->id_string => $label,\n ];\n }\n\n public function search(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->debugLog(\n $user,\n 'User extracted from request',\n ['user' => $user->getId(), 'tz' => $user->getTimezone()]\n );\n\n $searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());\n\n $this->debugLog(\n $user,\n 'ActivitySearch criteria built',\n ['searchCriteria' => $searchCriteria]\n );\n\n $filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);\n\n $this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);\n\n $this->validateSearch($request, $filterSet);\n\n $this->debugLog($user, 'Request validated');\n\n $searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);\n\n /** @var Collection<Activity> $activities */\n $activities = $searchResponse['results'];\n\n $this->debugLog($user, 'Activities ES response extracted');\n\n $hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(\n $user->getTeamId(),\n TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),\n );\n\n if ($hideInternalMeetingsSetting?->getValue() === '1') {\n $activities = $activities->filter(function (Activity $activity) {\n if ($activity->is_internal && empty($activity->actual_start_time)) {\n return false;\n }\n\n return true;\n });\n }\n\n $this->debugLog($user, 'Internal meetings (?!) filtered');\n\n $this->response->getManager()\n ->parseIncludes([\n 'category',\n 'organizer.group',\n 'prospect',\n 'stage',\n 'opportunity',\n 'stats',\n 'scorecards',\n 'masterTrack',\n 'activeParticipants',\n 'notification',\n ])\n ->setSerializer(new JsonSerializer());\n\n $transformerExcludes = $this->request->input('exclude');\n if ($transformerExcludes) {\n $this->response->getManager()->parseExcludes($transformerExcludes);\n }\n\n $this->debugLog($user, 'Response Manager (?!) applied');\n\n $transformer = new ActivityTransformer();\n $transformer->setConsumer($user);\n\n $this->debugLog($user, 'Activity Transformer added');\n\n $resource = new \\League\\Fractal\\Resource\\Collection($activities, $transformer);\n $page = $searchCriteria->getPageNumber();\n\n $this->debugLog($user, 'Search criteria page number called', ['page' => $page]);\n\n $histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');\n\n $this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);\n\n return $this->response->withArray([\n 'pagination' => [\n 'total' => $searchResponse['totalHits'],\n 'current' => $page,\n 'prev' => max($page - 1, 1),\n 'next' => $page + 1,\n ],\n 'results' => $this->response->getManager()->createData($resource)->toArray(),\n 'histogram' => $histogram,\n ]);\n }\n\n private function debugLog(User $user, string $logMessage, ?array $context = []): void\n {\n // Debug for Learning People Only\n if ($user->getTeamId() !== 260) {\n return;\n }\n\n Log::notice(\n sprintf('[activity-search-controller] %s', $logMessage),\n $context\n );\n }\n\n /** @throws ValidationException */\n private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void\n {\n $rules = [\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ];\n\n if ($prefix !== null && mb_strpos($prefix, '.') !== false) {\n $rules[rtrim($prefix, '.')] = sprintf(\n 'required|array|max:%d',\n $filterSet->count()\n );\n }\n\n $validationRules = $filterSet->getValidationRules($prefix)\n ->merge($rules)\n ->all();\n\n $request->validate($validationRules);\n }\n\n public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $search = $this->updateOrCreateActivitySearch($request);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function updateActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('update', $search);\n\n $this->updateOrCreateActivitySearch($request, $search);\n\n return $this->response->withOk();\n }\n\n private function storeNamedSearchFilters(\n Collection $request,\n Search $search,\n FilterDefinitionCollection $filterSet,\n ?string $prefix = null,\n ): self {\n $arrayTypeProperties = $filterSet\n ->getPropertyTypes([\n FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,\n ])\n ->all();\n\n $supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);\n\n foreach ($supportedRequestProperties as $requestPropertyName) {\n if (! array_has($request, $requestPropertyName)) {\n continue;\n }\n\n /** @var string|string[] $propertyValue */\n $propertyValue = array_get($request, $requestPropertyName);\n $propertyName = $prefix === null\n ? $requestPropertyName\n : mb_substr($requestPropertyName, mb_strlen($prefix));\n\n $isArrayType = array_has($arrayTypeProperties, $propertyName);\n\n if (! $isArrayType) {\n /** @var string $requestPropertyValue */\n\n $search->filters()->updateOrCreate(\n [\n 'filter' => $propertyName,\n ],\n [\n 'value' => $propertyValue,\n ]\n );\n\n continue;\n }\n\n /** @var string[] $requestPropertyValue */\n\n /** @var SearchFilter[]|Collection $existingFilterValues */\n $existingFilterValuesKeyed = $search->filters()\n ->where('filter', $propertyName)\n ->get()\n ->keyBy('id');\n\n // Iterate over values provided as request parameters\n foreach ($propertyValue as $value) {\n /** @var SearchFilter|null $valueFilter */\n $valueFilter = $search->filters()\n ->where(\n [\n 'filter' => $propertyName,\n 'value' => $value,\n ]\n )\n ->first();\n\n if ($valueFilter !== null) {\n // Remove filter value pair from list to be deleted\n $existingFilterValuesKeyed->forget($valueFilter->id);\n } else {\n // Add new filter/value pair\n $search->filters()->updateOrCreate([\n 'filter' => $propertyName,\n 'value' => $value,\n ]);\n }\n }\n\n // Delete filter value pairs for this filter that no longer exist in request parameters\n foreach ($existingFilterValuesKeyed as $existingFilter) {\n $existingFilter->delete();\n }\n }\n\n /** @var Collection<int, SearchFilter> $filtersKeyed */\n $filtersKeyed = $search->filters()->get()->keyBy('filter');\n\n // wipe removed filters from this search\n foreach ($filtersKeyed as $filterName => $filter) {\n if (array_has($request, $prefix . $filterName)) {\n continue;\n }\n\n // Remove all filter values for this filter\n $search->filters()->where('filter', $filterName)->delete();\n }\n\n return $this;\n }\n\n /**\n * @throws AuthorizationException\n */\n public function fetchActivitySearch(\n Search $search,\n Request $request,\n SearchTransformer $searchTransformer,\n ): JsonResponse {\n $this->authorize('view', $search);\n\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $search->loadCount('automatedReports');\n\n return $this->response->withItem(\n $search,\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection(\n $user->searches()->withCount('automatedReports')->get(),\n $searchTransformer\n ->withConsumer($user)\n );\n }\n\n /**\n * Deletes a saved search\n *\n * @param Request $request\n * @param Search $search\n *\n * @throws Exception\n *\n * @return JsonResponse\n */\n public function deleteActivitySearch(Request $request, Search $search): JsonResponse\n {\n $this->authorize('delete', $search);\n\n // Disable and orphan any AutomatedReports that use this search\n $search->automatedReports()->withTrashed()->update([\n 'activity_search_id' => null,\n 'status' => false,\n ]);\n\n // Delete filters and the search itself\n $search->filters()->delete();\n $search->delete();\n\n return $this->response->withOk();\n }\n\n public function live(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n $user = $this->getUserFromRequest($request);\n\n $this->request->validate([\n 'sort_direction' => 'in:asc,desc',\n 'limit' => 'integer|min:1|max:50',\n 'page' => 'integer|min:1',\n ]);\n\n $activities = $repository->getLiveCoachingEligibleActivities(\n user: $user,\n lookBackMinutes: self::LOOK_BACK,\n limit: (int) $this->request->input('limit', 25),\n page: (int) $this->request->input('page', 1),\n sortBy: ['actual_start_time', 'scheduled_start_time'],\n sortDirection: (string) $this->request->input('sort_direction', 'asc'),\n );\n\n $this->response\n ->getManager()\n ->parseIncludes(['organizer.group', 'prospect'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($activities, new ActivityTransformer());\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function show(Activity $activity, ActivityService $activityService): JsonResponse\n {\n $this->authorize('show', $activity);\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n // Sync the opportunity with the latest data if possible.\n if ($activity->opportunity_id) {\n try {\n $crmService = $this->providerRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($team->getOwner());\n } else {\n $crmService->setUser($user);\n }\n\n $crmService->syncOpportunity($activity->opportunity->crm_provider_id);\n } catch (Exception $exception) {\n // Move on.\n }\n }\n\n $activityData = $activityService->getActivityData($this->request->user(), $activity);\n\n return response()->json($activityData);\n }\n\n public function createRecording(Activity $activity)\n {\n $this->authorize('record', $activity);\n\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Tell Twilio to start recording this activity.\n if ($activity->recording_state === Activity::RECORDING_OFF) {\n $job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withCreated();\n }\n\n return $this->response->errorGone('Activity is already recording.');\n }\n\n public function updateRecording(Request $request, Activity $activity)\n {\n $this->authorize('record', $activity);\n\n $request->validate([\n 'preference' => 'boolean',\n 'state' => [\n 'string',\n Rule::in([\n Activity::RECORDING_IN_PROGRESS,\n Activity::RECORDING_PAUSED,\n ]),\n ],\n ]);\n\n if ($request->has('state')) {\n if ($activity->hasRecordingReasonComplianceRestricted()) {\n return $this->response->errorGone('Recording this number has been disabled by your organization.');\n }\n\n // Toggle the recording state between paused and resumed.\n if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {\n $job = (new ToggleRecording($activity, $request->input('state')))\n ->onQueue(Constants::QUEUE_CONFERENCES);\n\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Recording is not toggleable.');\n }\n\n if ($request->has('preference')) {\n $activity->update([\n 'recording_preference' => $request->input('preference') ? 1 : 0,\n ]);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorWrongArgs('Something went wrong');\n }\n\n public function stopRecording(Activity $activity)\n {\n $this->authorize('stopRecord', $activity);\n\n // Tell Twilio to stop recording this activity.\n if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {\n $job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);\n dispatch($job);\n\n return $this->response->withOk();\n }\n\n return $this->response->errorGone('Activity is not recording.');\n }\n\n /**\n * Add activity to this user's favorites playlist\n *\n * @throws AuthorizationException\n */\n public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse\n {\n $this->authorize('favorite', $activity);\n\n $user = $this->getUserFromRequest($this->request);\n $favorite = $activity->wasFavoritedBy($user);\n $name = $activity->activity_title ?? '';\n\n // It needs to check at least one record.\n if (! $favorite) {\n $favoritePlaylist = $user->favoritePlaylist();\n\n $playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(\n $activity,\n $user,\n $favoritePlaylist\n );\n\n if ($playlistActivity !== null) {\n $playlistActivity->update(\n // Just update, don't sort.\n ['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],\n );\n } else {\n $playlistActivity = $activity->playlistActivities()->create([\n 'playlist_id' => $favoritePlaylist->getId(),\n 'user_id' => $user->getId(),\n 'start_time' => 0,\n 'name' => mb_strimwidth($name, 0, 100),\n ]);\n // Sort it on top.\n $playlistActivity->update(\n [\n 'sort' => $playlistActivityRepository->calculateNewSortOrder(\n null,\n $playlistActivity,\n ),\n ],\n );\n }\n\n $playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);\n\n return new JsonResponse([], JsonResponse::HTTP_CREATED);\n }\n\n return new JsonResponse(\n [\n 'error' => [\n 'code' => AbstractResponse::CODE_CONFLICT,\n 'http_code' => JsonResponse::HTTP_CONFLICT,\n 'message' => 'Resource Already Exists',\n ],\n ],\n JsonResponse::HTTP_CONFLICT,\n );\n }\n\n /**\n * Remove activity from this user's favorites playlist\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unfavorite(Activity $activity)\n {\n $user = $this->request->user();\n\n $favorites = $activity->favoritedBy($user);\n\n if ($favorites && $favorites->isEmpty()) {\n return $this->response->errorNotFound('Favorite not found.');\n }\n\n $this->authorize('unfavorite', [$activity, $favorites]);\n\n // When you unfavorite an activity,\n // it should remove all the activities in it, including snippets.\n $isDeleted = $favorites->each(function ($favorite) {\n $favorite->forceDelete();\n });\n\n if ($isDeleted) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not remove favorite.');\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function notify(Activity $activity)\n {\n $this->authorize('notify', $activity);\n\n $user = $this->request->user();\n\n $existingNotification = $activity->availabilityNotifications()\n ->where('user_id', $user->id)\n ->exists();\n\n if ($existingNotification) {\n return $this->response->errorWrongArgs('Notification is already configured.');\n }\n\n $notification = Activity\\AvailabilityNotification::create([\n 'user_id' => $user->id,\n 'activity_id' => $activity->id,\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($notification, new AvailabilityNotificationTransformer());\n }\n\n /**\n * @param Activity $activity\n * @param Activity\\AvailabilityNotification $notification\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function unnotify(Activity $activity, Activity\\AvailabilityNotification $notification)\n {\n $this->authorize('unnotify', [$activity, $notification]);\n\n if ($notification->sent_at || $notification->delete()) {\n return $this->response->withNoContent();\n }\n\n return $this->response->errorGone('Could not delete notification.');\n }\n\n public function play(Request $request, Activity $activity)\n {\n $this->authorize('stream', $activity);\n\n $request->validate([\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $activity->plays()->create([\n 'user_id' => $user->getId(),\n 'start_time' => $request->input('start_time'),\n ]);\n\n return $this->response->withCreated();\n }\n\n /**\n * @param Activity $activity\n *\n * @return mixed\n */\n public function comment(Activity $activity)\n {\n return $this->newComment($activity);\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @return mixed\n */\n public function replyComment(Activity $activity, Comment $comment)\n {\n return $this->newComment($activity, $comment);\n }\n\n /**\n * @param Activity $activity\n * @param Comment|null $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n protected function newComment(Activity $activity, ?Comment $comment = null)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n 'type' => 'integer|between:0,3',\n 'visibility' => sprintf('nullable|integer|between:1,%d', count(Comment::getVisibilityLevels())),\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n $threadStartId = null;\n if ($comment) {\n $threadStartId = $comment->thread_start_id ?: $comment->id;\n }\n\n try {\n $newComment = Comment::create([\n 'parent_comment_id' => $comment->id ?? null,\n 'thread_start_id' => $threadStartId,\n 'activity_id' => $activity->id,\n 'user_id' => $this->request->user()->id,\n 'comment' => trim($this->request->input('comment')),\n 'start_time' => $this->request->input('start_time', 0),\n 'end_time' => $this->request->input('end_time', 0),\n 'type' => $this->request->input('type', Comment::TYPE_NEUTRAL),\n 'visibility' => $this->request->input('visibility', Comment::VISIBILITY_PUBLIC),\n ]);\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($newComment, new ActivityCommentTransformer());\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not create comment.' . $exception->getMessage());\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function updateComment(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'comment' => 'required|between:1,5000',\n //'start_time' => 'numeric|between:0,'.$activity->duration,\n //'end_time' => 'required_with:start_time|greater_than_or_equal:start_time|numeric|between:0,'.$activity->duration,\n ]);\n\n try {\n $comment->update([\n 'comment' => trim($this->request->input('comment')),\n ]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment.');\n }\n }\n\n public function updateCommentVisibility(Activity $activity, Comment $comment)\n {\n $this->authorize('comment', [$activity, $comment]);\n\n $this->request->validate([\n 'visibility' => sprintf('integer|between:1,%d', count(Comment::getVisibilityLevels())),\n ]);\n\n $visibility = $this->request->input('visibility');\n\n if ($comment->parent !== null) {\n return $this->response->errorWrongArgs('Comment visibility can only be updated on top level comments.');\n }\n\n try {\n $this->activityCommentService->updateCommentVisibility($comment, $visibility);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withOk();\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n return $this->response->errorInternalError('Could not update comment\\'s visibility.');\n }\n }\n\n /**\n * @param Activity $activity\n * @param Comment $comment\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function deleteComment(Activity $activity, Comment $comment)\n {\n $this->authorize('deleteComment', [$activity, $comment]);\n\n // Delete comment and any children.\n $comment->delete();\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function fetchComments()\n {\n $user = $this->request->user();\n $this->request->validate([\n 'forUserId' => 'uuid:users,team_id,' . $user->team_id,\n 'types' => 'array',\n 'types.*' => 'integer|between:0,3',\n ]);\n $forUser = null;\n\n $types = [Comment::TYPE_NEUTRAL, Comment::TYPE_GAME_CHANGER, Comment::TYPE_POSITIVE];\n $user = $this->request->user();\n if ($this->request->has('forUserId')) {\n $forUser = $user->team->users()->uuid($this->request->input('forUserId'));\n }\n\n $comments = Comment::query()\n ->whereHas('activity', static function (Builder $builder) use ($user, $forUser): void {\n $builder\n // I left feedback on my own activity; or\n ->where('activities.user_id', $user->getId());\n if ($forUser) {\n // I left feedback on any activity for this user.\n $builder->orWhere([\n 'user_id' => $user->getId(),\n 'activities.user_id' => $forUser->getId(),\n ]);\n }\n })\n ->whereIn('type', $this->request->input('types', $types))\n ->orderBy('created_at', 'desc')\n ->get();\n\n $this->response\n ->getManager()\n ->parseIncludes(['activity', 'user'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($comments, new ActivityCommentTransformer());\n }\n\n public function deleteCoachingFeedback(Activity $activity, CoachingFeedback $coachingFeedback)\n {\n $this->authorize('deleteCoachingFeedback', [$activity, $coachingFeedback]);\n $activity = $coachingFeedback->getActivity();\n\n if ($coachingFeedback->delete()) {\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'delete-coaching-feedback',\n ));\n\n return $this->response->withOk();\n }\n\n return $this->response->withError('Delete operation failed. Contact support.', 500);\n }\n\n /**\n * Add new or update Coaching feedback\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n * @throws \\Illuminate\\Validation\\ValidationException\n *\n * @return mixed\n */\n public function putCoachingFeedback(Request $request, Activity $activity)\n {\n $user = $request->user();\n\n if (! $user instanceof User) {\n abort(403);\n }\n $teamId = $user->getTeamId();\n\n $this->authorize('coach', $activity);\n\n $this->request->validate([\n 'coach_id' => 'required|uuid:users,team_id,' . $teamId,\n 'coachee_id' => 'required|uuid:users,team_id,' . $teamId,\n 'visibility' => ['required', Rule::in(CoachingFeedback::VISIBILITIES)],\n 'coaching_sections.*.uuid' => 'required|uuid:coaching_sections',\n 'coaching_sections.*.score' => ['required', Rule::in(CoachingSectionFeedback::SCORES)],\n 'coaching_sections.*.summary' => 'string|max:10000',\n 'coaching_sections.*.criteria.*.uuid' => 'required|uuid:coaching_section_criteria',\n 'coaching_sections.*.criteria.*.note' => 'required|string|max:10000',\n 'sharedWithUsers' => [\n 'required_if:visibility,' . CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS,\n 'array',\n ],\n 'sharedWithUsers.*' => [\n 'uuid:users,team_id,' . $teamId,\n ],\n ]);\n\n /** @var User $coach */\n $coach = User::uuid($this->request->input('coach_id'));\n /** @var User $coachee */\n $coachee = User::uuid($this->request->input('coachee_id'));\n $coachingSectionFeedbacks = $this->request->input('coaching_sections');\n\n $previousRecord = $this->coachingFeedbackRepository->getOneForActivityByCoacheeAndCoach(\n $coachee->getId(),\n $coach->getId(),\n $activity->getId()\n );\n $recordIsNew = false;\n if ($previousRecord === null) {\n $recordIsNew = true;\n }\n\n if (! $coachee->isSameTeamId($coach)) {\n return $this->response->errorForbidden('User not member of your team.');\n }\n\n if (! is_array($coachingSectionFeedbacks) || count($coachingSectionFeedbacks) < 1) {\n return $this->response->withError('At least one Coaching Framework Section shall be scored.', 422);\n }\n\n if (! $activity->participants()->where('participants.user_id', $coachee->id)->exists()) {\n return $this->response->withError('Coached user did not participate activity.', 422);\n }\n\n $visibility = $this->request->input('visibility');\n\n $shouldSendNotification = $recordIsNew;\n if ($recordIsNew === false && $visibility !== $previousRecord->getVisibility()) {\n $shouldSendNotification = true;\n }\n\n /**\n * Create CoachingFeedback\n *\n * @var CoachingFeedback $coachingFeedback\n */\n $coachingFeedback = $activity->coachingFeedbacks()->updateOrCreate(\n [\n 'coach_id' => $coach->id,\n 'coachee_id' => $coachee->id,\n ],\n [\n 'framework_id' => $activity->category->id,\n 'visibility' => $visibility,\n ]\n );\n\n $sharedUserIds = [];\n if ($visibility === CoachingFeedback::VISIBLE_TO_SPECIFIC_USERS) {\n foreach ($this->request->input('sharedWithUsers') as $sharedWithUserUuid) {\n /** @var User $user */\n $user = User::uuid($sharedWithUserUuid);\n $sharedUserIds[] = $user->getId();\n }\n }\n\n $syncResult = $coachingFeedback->customAccessUsers()->sync($sharedUserIds);\n\n $scores = [];\n\n\n /**\n * Create CoachingSectionsFeedbacks.\n *\n * @var CoachingSectionFeedback $coachingSectionFeedback\n */\n foreach ($coachingSectionFeedbacks as $coachingSectionFeedbackInput) {\n $coachingSection = CoachingSection::uuid($coachingSectionFeedbackInput['uuid']);\n $coachingSectionFeedback = $coachingFeedback->sectionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_id' => $coachingSection->id,\n ],\n [\n 'score' => array_get($coachingSectionFeedbackInput, 'score'),\n 'summary' => array_get($coachingSectionFeedbackInput, 'summary') ?? '',\n ]\n );\n\n $scores[] = array_get($coachingSectionFeedbackInput, 'score');\n\n $criteria = array_get($coachingSectionFeedbackInput, 'criteria');\n if (is_array($criteria) && ! empty($criteria)) {\n foreach ($criteria as $criteriaFeedbackInput) {\n $coachingSectionFeedback->criterionFeedbacks()->updateOrCreate(\n [\n 'coaching_section_criterion_id' => CoachingSectionCriterion::uuid(array_get($criteriaFeedbackInput, 'uuid'))\n ->id,\n ],\n ['note' => array_get($criteriaFeedbackInput, 'note')],\n );\n }\n }\n }\n\n $coachingFeedback->average_score = array_sum($scores) / count($scores);\n\n if ($recordIsNew === false && $coachingFeedback->getAverageScore() !== $previousRecord->getAverageScore()) {\n $shouldSendNotification = true;\n }\n if (! empty($syncResult['attached']) || ! empty($syncResult['detached']) || ! empty($syncResult['updated'])) {\n $shouldSendNotification = true;\n }\n\n $coachingFeedback->save();\n // ensure updated at for coaching feedback on section feedback summary added.\n $coachingFeedback->touch();\n\n if ($shouldSendNotification) {\n event(new Coached($coachingFeedback));\n }\n\n Datadog::increment('jiminny.activity.score.update', 1, ['company' => $activity->user->team->slug]);\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n $coachingFeedbackTransformer = new CoachingFeedbackTransformer();\n $coachingFeedbackTransformer->setConsumer($this->getUserFromRequest($request));\n\n return $this->response->withItem($coachingFeedback, $coachingFeedbackTransformer);\n }\n\n\n /**\n * Retrieve category criteria for coaching.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachingSections(Activity $activity)\n {\n $this->authorize('coach', $activity);\n\n if ($activity->category === null) {\n return $this->response->errorUnprocessable('Category has not yet been assigned.');\n }\n\n $criteria = $activity\n ->category\n ->coachingSections()\n ->where('is_enabled', 1)\n ->orderBy('sequence', 'asc');\n\n $this->response\n ->getManager()\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withCollection($criteria->get(), new CoachingSectionsTransformer());\n }\n\n /**\n * @throws AuthorizationException\n * @throws ValidationException\n *\n * @return mixed\n */\n public function addToPlaylist(Activity $activity, PlaylistTrackFactoryInterface $playlistTrackFactory)\n {\n $this->request->validate([\n 'playlists' => 'required|array',\n 'playlists.*' => 'uuid:playlists',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'name' => 'required|max:100',\n ]);\n\n $this->authorize('addToPlaylist', [$activity, $this->request->input('playlists')]);\n\n $startTime = $this->request->input('start_time');\n $endTime = $this->request->input('end_time');\n $name = $this->request->input('name');\n /** @var User $user */\n $user = $this->request->user();\n\n // Get playlist by uuid.\n foreach ($this->request->input('playlists') as $playlistId) {\n // Pull out the playlist model.\n $playlist = Playlist::uuid($playlistId);\n\n $playlistTrackFactory->createTrack($playlist, $user, [\n 'name' => $name,\n 'activity' => $activity,\n 'start_time' => $startTime,\n 'end_time' => $endTime,\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function share(Request $request, Activity $activity): JsonResponse\n {\n $this->authorize('share', $activity);\n\n $request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'recipients.*.type' => 'in:user,group',\n 'recipients.*.id' => 'string|max:40',\n 'share' => 'string|max:255',\n ]);\n\n $user = $request->user();\n\n $recipients = $request->get('recipients');\n $users = $this->userService->convertRecipientsToUsers($user, $recipients);\n\n $shareData = [\n 'from_user_id' => $user->id,\n 'note' => $request->input('note'),\n 'start_time' => $request->input('start_time'),\n 'end_time' => $request->input('end_time'),\n ];\n\n // Create a share object against a notification provider channel\n if ($request->input('share')) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'notification_provider_channel' => $request->input('share'),\n ]\n )\n );\n\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n\n // Create a share object against each recipient\n foreach ($users as $recipient) {\n /** @var Share $share */\n $share = $activity->shares()->create(\n array_merge(\n $shareData,\n [\n 'to_user_id' => $recipient->id,\n ]\n )\n );\n\n // If parent_share_id has been selected yet\n if (! isset($shareData['parent_share_id'])) {\n // All subsequent shares need to reference this row as parent_share_id\n $shareData['parent_share_id'] = $share->id;\n }\n }\n\n return $this->response->withOk();\n }\n\n /**\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function coachRequest(Activity $activity)\n {\n $this->authorize('coachRequest', $activity);\n\n $this->request->validate([\n 'note' => 'string|max:1000',\n 'start_time' => 'numeric|between:0,' . $activity->duration,\n 'end_time' => 'nullable|greater_than_or_equal:start_time|numeric|between:0,' . $activity->duration,\n 'coachers.*.type' => 'required|in:user',\n 'coachers.*.id' => 'required',\n ]);\n\n $coachers = $this->request->get('coachers');\n $user = $this->request->user();\n $users = $this->userService->convertRecipientsToUsers($user, $coachers);\n\n foreach ($users as $coacher) {\n CoachRequest::create([\n 'user_id' => $coacher->id,\n 'activity_id' => $activity->id,\n 'note' => $this->request->get('note'),\n 'start_time' => $this->request->get('start_time'),\n 'end_time' => $this->request->get('end_time'),\n ]);\n }\n\n return $this->response->withOk();\n }\n\n public function createActivityTopicTriggers(Activity $activity, LoggerInterface $logger): HttpFoundation\\JsonResponse\n {\n $this->authorize('analyzeTopicTriggers', $activity);\n\n if (! $activity->hasTranscription()) {\n return new HttpFoundation\\JsonResponse(\n [\n 'error' => 'Transcription not found.',\n ],\n JsonResponse::HTTP_NOT_FOUND\n );\n }\n\n $logger->info(__METHOD__ . ': queued for analysis', [\n 'activity' => $activity->id_string,\n ]);\n\n dispatch(new ActivityAnalytics\\Job\\AnalyzeActivityTopicTriggers($activity));\n\n return new HttpFoundation\\JsonResponse(null, JsonResponse::HTTP_CREATED);\n }\n\n public function fetchActivityTopicTriggers(\n Activity $activity,\n LoggerInterface $logger,\n ActivityTopicTriggerTransformer $transformer\n ): HttpFoundation\\JsonResponse {\n $this->authorize('fetchTopicTriggers', $activity);\n\n $logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n if (! $activity->isProcessed()) {\n return new HttpFoundation\\JsonResponse([]);\n }\n\n $payload = [];\n\n if ($activity->hasTopicTriggers()) {\n $payload = $activity->getTopicTriggersSorted()\n ->map(\n static fn (Activity\\TopicTrigger $activityTopicTrigger): array\n => $transformer->transform($activityTopicTrigger)\n )\n ->values()\n ->all();\n }\n\n return new HttpFoundation\\JsonResponse($payload);\n }\n\n /**\n * @param Activity $activity\n * @param StatsTransformer $statsTransformer\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function stats(Activity $activity, StatsTransformer $statsTransformer)\n {\n $this->authorize('stream', $activity);\n\n if (! $activity->hasTranscription()) {\n return $this->response->errorNotFound('Waveform data is not yet generated.');\n }\n\n $this->response\n ->getManager()\n ->parseIncludes(['wavedata'])\n ->setSerializer(new JsonSerializer());\n\n return $this->response->withItem($activity, $statsTransformer);\n }\n\n public function destroy(Activity $activity)\n {\n $this->authorize('delete', $activity);\n\n $activity->delete();\n\n \\Log::info('Soft delete activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);\n\n return $this->response->withNoContent();\n }\n\n public function note(Activity $activity)\n {\n $this->authorize('note', $activity);\n\n $this->request->validate([\n 'note' => 'required|min:1|max:2000',\n 'time' => 'required|numeric|min:0|max:86400',\n ]);\n\n $note = $this->request->input('note');\n $time = $this->request->input('time');\n\n $this->activityService->setActivity($activity);\n $this->activityService->takeNote($this->getUser(), $note, $time);\n\n return $this->response->withCreated();\n }\n\n /**\n * Mark an activity as private.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPrivate(Activity $activity)\n {\n $this->authorize('markAsPrivate', $activity);\n\n if ($activity->is_private === false) {\n $activity->is_private = true;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * Mark an activity as public.\n *\n * @param Activity $activity\n *\n * @throws AuthorizationException\n *\n * @return mixed\n */\n public function markAsPublic(Activity $activity)\n {\n $this->authorize('markAsPublic', $activity);\n\n if ($activity->is_private) {\n $activity->is_private = false;\n $activity->save();\n\n return $this->response->withOk();\n }\n\n return $this->response->withNoContent();\n }\n\n /**\n * @throws LogicException\n */\n public function fetchCloudFrontS3MediaKeys(Activity $activity, PlaybackService $playbackService): JsonResponse\n {\n $masterTrack = $activity->masterTrack()->first();\n\n if (! $masterTrack instanceof Track) {\n throw new LogicException(sprintf('Master track not found for activity \"%s\"', $activity->getUuid()));\n }\n\n return $this->response->withArray(\n $playbackService->generateCookies(\n $masterTrack,\n $this->request->ip(),\n ),\n );\n }\n\n /**\n * @throws ValidationException\n */\n private function updateOrCreateActivitySearch(Request $request, ?Search $search = null): Search\n {\n $request->validate([\n 'name' => 'required|string|min:2|max:100',\n ]);\n\n $user = $this->getUserFromRequest($request);\n\n $searchName = $request->input('name');\n\n if ($search !== null) {\n $search->update([\n 'name' => $searchName,\n ]);\n\n return $search;\n }\n\n $request->validate([\n 'filters' => ['required', 'array', new MultidimensionalArrayMaxCharRule(limit: 255)],\n 'nudges' => 'array|max:' . count(Nudge::MAP_CHANNEL),\n 'nudges.*.channel' => 'required|in:' . implode(',', Nudge::MAP_CHANNEL),\n 'nudges.*.frequency' => 'required|in:' . implode(',', Nudge::MAP_FREQUENCY),\n 'nudges.*.expiresAt' => [\n 'required',\n 'date',\n 'after:today',\n 'before_or_equal:' . now()->addYear()->format('Y-m-d'),\n ],\n ]);\n\n $searchCriteria = Criteria::createFromRequest(\n Collection::make($request->input('filters', []))->all(),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($searchCriteria, $user);\n $this->validateSearch($request, $filterSet, 'filters.');\n\n /** @var Search $search */\n $search = Search::create([\n 'name' => $searchName,\n 'uuid' => Uuid::uuid4()->toString(),\n 'user_id' => $user->getId(),\n ]);\n\n Collection::make($request->input('nudges', []))\n ->each(fn (array $attributes): Nudge => $this->nudgeFactory->createNudge($search, $attributes));\n\n $this->storeNamedSearchFilters(Collection::make($request->all()), $search, $filterSet, 'filters.');\n\n return $search;\n }\n\n private function resolveAccount(\n Team $team,\n Contact $contact,\n ServiceInterface $crmService,\n array $prospects,\n ): ?Account {\n $this->logger->info('Resolving account from contact');\n $account = $contact->getAccount();\n\n if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS)) {\n $this->logger->info('Team does not have feature to link activity to multiple prospects');\n\n return $account;\n }\n\n $this->logger->info('Resolving account from prospect data');\n $accountData = array_filter(\n $prospects,\n static fn (array $prospectData): bool => $prospectData['type'] === 'account'\n );\n\n if (! empty($accountData)) {\n $this->logger->info('Found account data in prospects');\n $accountData = reset($accountData);\n\n $account = $team->crm->accounts()->where('crm_provider_id', $accountData['id'])->first();\n\n if (! $account instanceof Account) {\n $this->logger->info('Account not found in database, syncing from CRM');\n $account = $crmService->syncAccount($accountData['id']);\n }\n }\n\n $this->logger->info('Resolved account', ['account' => $account->getId()]);\n\n return $account;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.42785904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.43650267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4474734,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.45611703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.46476063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.47573137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4867021,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.51329786,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5242686,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.66921544,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68085104,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.69015956,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.7017952,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.7117686,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;\n\nSELECT * FROM activities WHERE uuid_to_bin('f43cf158-e60d-46e5-92f8-c4e0594a3219') = uuid; # 422003\nSELECT * FROM activities WHERE id IN (16,422003);\nSELECT * FROM activities where status = 'failed';\n\nSELECT * FROM tracks WHERE activity_id = 422003;\n\nSELECT\n a.*\nFROM activities a\nJOIN users u ON a.user_id = u.id\nWHERE\n a.status = 'completed'\n AND uuid_to_bin('641f1acb-16b8-42d1-8726-df52979dad0e') = u.uuid\n AND a.deleted_at IS NULL\n AND EXISTS (\n SELECT 1 FROM tracks t\n WHERE t.activity_id = a.id\n AND t.type IN ('audio', 'video')\n )\nORDER BY a.actual_start_time DESC\nLIMIT 25;\n\nselect * from teams where id = 19;\nselect * from crm_configurations where provider = 'pipedrive';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 19 and sa.provider = 'pipedrive';\n\nSELECT * FROM social_accounts WHERE id = 1116;\n\nUPDATE social_accounts SET provider_user_token = 'v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA',\nprovider_refresh_token = '5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc',\nexpires = 1779091997,\nstate = 'connected'\nWHERE id = 1116;\n\n \"provider_user_token\": \"v1u:AQIBAHj-LzTNK2yuuuaLqifzhWb9crUNKTpk4FlQ9rjnXqp_6AEQhDhDQVa1nvWCHEvnpvSEAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMnG8KNcZLIjEnlRPxAgEQgDsGPIcKjsMU7Qel36BtM5FCQa56mYUy24_AAoqd12yjVFkq6egLqS0inp-5G4JE7frJURMV8VTw_fY14g:rYyABXmXsBhEdYU_dfmDH8GF-vzSseJXE5bds_zAyVdAwlTXOPdTl9i4PS4jVofLvpq7IRgvEt2BzGhR6cWiQXHHD0AtayQOkZm262ClFCsMYtKGfep0Jq1n0eiRIVqT9gAY7rWvhpEDF8oAlgnRGmqx-euIkpzE79sPXh4OjNx_yUIaanjyplGpBBJ_NiACd1sMlZmseyUyyU_gldqlCBAuK9hhATc9icgg7zuoc7nKBBrlg49tRtzEagDh6xbFBpzfCp7kE_7n4TLWfWHLPMzu-bktjwA969G9sFRmoH1GjPA0a2odDT4dk1_1ouBrB1NcTT2hQUqH-_RzDW2nWmeoVHA\",\n \"provider_refresh_token\": \"5034113:19555731:87c14258f0c813d02767ee975f6044d46b6b2bfc\",\n \"expires\": 1779091997,","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6884959928424888601
|
-8385843421312740204
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12098 on JY-20676-delete Project: faVsco.js, menu
#12098 on JY-20676-delete-report-related-objects, menu
Start Listening for PHP Debug Connections
AutomatedReportsServiceTest
Run 'AutomatedReportsServiceTest'
Debug 'AutomatedReportsServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Http\Controllers\API;
use Carbon\Carbon;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Jiminny\Component\ActivityAnalytics;
use Jiminny\Component\ActivitySearch;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Queue\Constants;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Contracts\Nudge\NudgeFactoryInterface;
use Jiminny\Contracts\Playlist\PlaylistTrackFactoryInterface;
use Jiminny\Contracts\Repositories\PlaylistActivityRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Enums\TeamSetting;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Events\Activities\Coaching\Coached;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Http\Controllers\API\BaseController as Controller;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Responses\Api\AbstractResponse;
use Jiminny\Http\Responses\Api\Response;
use Jiminny\Http\Serializers\JsonSerializer;
use Jiminny\Http\Transformers\ActivityCommentTransformer;
use Jiminny\Http\Transformers\ActivityTopicTriggerTransformer;
use Jiminny\Http\Transformers\ActivityTransformer;
use Jiminny\Http\Transformers\AvailabilityNotificationTransformer;
use Jiminny\Http\Transformers\CoachingFeedbackTransformer;
use Jiminny\Http\Transformers\CoachingSectionsTransformer;
use Jiminny\Http\Transformers\SearchTransformer;
use Jiminny\Http\Transformers\StatsTransformer;
use Jiminny\Jobs\Crm\SaveActivity;
use Jiminny\Jobs\Crm\UpdateStage;
use Jiminny\Jobs\Telephony\StartRecording;
use Jiminny\Jobs\Telephony\StopRecording;
use Jiminny\Jobs\Telephony\ToggleRecording;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\CoachingSectionCriterion;
use Jiminny\Models\CoachingSectionFeedback;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\LayoutEntity;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\LanguageDialect;
use Jiminny\Models\Lead;
use Jiminny\Models\Nudge;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
use Jiminny\Repositories\CoachingFeedbackRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Rules\CrmReference;
use Jiminny\Rules\MultidimensionalArrayMaxCharRule;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\PlaybackService;
use Jiminny\Services\UserService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Sentry;
use Symfony\Component\HttpFoundation;
final class ActivityController extends Controller implements CommentContextInterface
{
// Number of minutes to look back on activities. i.e. a timeout on activity duration.
private const int LOOK_BACK = 180;
public function __construct(
private ProviderRegistry $providerRegistry,
private ActivityService $activityService,
Response $response,
private UserService $userService,
private ActivitySearch\Service\ActivitySearch $activitySearch,
private NudgeFactoryInterface $nudgeFactory,
private ActivityCommentService $activityCommentService,
private LoggerInterface $logger,
private readonly CoachingFeedbackRepository $coachingFeedbackRepository,
private readonly TeamRepository $teamRepository,
) {
parent::__construct($response);
}
public static function getCommentImplementation(): string
{
return Comment::class;
}
public function delete()
{
$this->request->validate([
'*' => 'uuid:activities',
]);
$deletedIds = [];
foreach ($this->request->all() as $activityId) {
$activity = Activity::idOrUuId($activityId);
try {
if ($this->authorize('delete', $activity)) {
$activity->delete();
$deletedIds[] = $activityId;
\Log::info('Soft deleted activity ' . $activity->id_string . ' by user ' . $this->getUser()->id);
}
} catch (AuthorizationException $authorizationException) {
// They didn't have permission.
}
}
return $this->response->withArray($deletedIds);
}
public function update(Request $request, Activity $activity)
{
$this->authorize('updateMetadata', $activity);
$request->validate([
'title' => 'string|max:250',
'category_id' => 'uuid:playbook_categories',
'language' => [
new In(
LanguageDialect::query()
->with('language')
->cursor()
->map(static function (LanguageDialect $languageDialect): string {
return $languageDialect->getLanguageLocale();
})
->all()
),
],
]);
if ($request->has('title')) {
$activity->title = $request->input('title');
}
if ($request->has('category_id')) {
$category = PlaybookCategory::uuid($request->input('category_id'));
if ($category->playbook->team_id !== $request->user()->team_id) {
return $this->response->errorNotFound('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
if ($request->has('language')) {
if (! $activity->isInProgress()) {
return $this->response->withError(
'Activity language can only be set while the meeting is in progress.',
400
);
}
$activity->setLanguageCode($request->input('language'));
}
$activity->save();
return $this->response->withOk();
}
// XXX: This should be merged with the update method.
/**
* @param Activity $activity
*
* @throws AuthorizationException
* @throws SocialAccountTokenInvalidException
*
* @return mixed
*/
public function summarize(Activity $activity): mixed
{
$this->logger->info('[Log Activity] Summarizing activity ', [
'activityId' => $activity->getUuid(),
'payload' => $this->request->all(),
]);
$this->authorize('update', $activity);
$this->logger->info('[Log Activity] Validating summary');
// Validate the payload.
$this->validateSummary($activity);
// All objects must belong to this team.
/** @var User $user */
$user = $this->request->user();
$team = $user->getTeam();
$crmService = $this->providerRegistry->get($team->crm->provider);
try {
$crmUser = $user;
if ($user->isCrmRequired() === false) {
$crmUser = $team->owner;
}
$crmService->setUser($crmUser);
} catch (SocialAccountTokenInvalidException $accountTokenInvalidException) {
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($accountTokenInvalidException->getMessage());
}
$rawEntities = $this->request->input('entities');
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid(
$this->request->input('layout_id')
);
// Delay execution of CRM jobs to avoid locking issues.
$jobDelay = 0;
// If we have arrived from a notification, mark it as read.
$notificationId = $this->request->input('nId');
if ($notificationId) {
$notification = $user->unreadNotifications->where('id', $notificationId)->first();
if ($notification) {
$notification->markAsRead();
}
}
$title = $this->request->input('title');
$prospects = $this->request->input('prospects');
$opportunityId = $this->request->input('opportunity_id');
$stageId = $this->request->input('stage_id');
$categoryId = $this->request->input('category_id');
$summary = $this->request->input('summary');
$crmProviderId = $this->request->input('crm_id');
$isInternal = $this->request->input('is_internal') ?? false;
$lead = null;
$category = null;
$account = null;
$contact = null;
$opportunity = null;
$stage = null;
$callStage = null;
foreach ($prospects as $prospectData) {
$objectId = $prospectData['id'];
if ($objectId === null) {
continue;
}
$objectType = $prospectData['type'];
$this->logger->info('debug', ['prospect_data' => $prospectData]);
try {
if ($objectType === null) {
$this->logger->info('no object type');
if ($crmService instanceof SupportsObjectTypeParseInterface) {
$objectType = $crmService->parseObjectType($objectId);
}
}
switch ($objectType) {
case 'lead':
$this->logger->info('Processing lead');
/** @var Lead|null $lead */
$lead = $team->crm->leads()->where('crm_provider_id', $objectId)->first();
// Lead does not exist locally, import it.
if ($lead === null) {
$this->logger->info('Lead does not exist locally');
/** @var Lead $lead */
$lead = $crmService->syncLead($objectId);
}
$this->logger->info('Lead found', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
if ($stageId === null) {
$this->logger->info('Stage ID is null');
// If it was not provided, just assume it is the current stage.
$callStage = $lead->stage;
break;
}
$this->logger->info('Looking for stage');
// Determine if they have changed the stage.
/** @var Stage $stage */
$stage = $team->crm->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_LEAD)
->firstOrFail();
$this->logger->info('Stage found', ['stageId' => $stage->id, 'lead_stage' => $lead->stage_id]);
if ($lead->stage_id && $lead->stage_id !== $stage->id) {
$this->logger->info('Stage has changed');
// Storage current stage on activity.
$callStage = $lead->stage;
// The stage has changed, update in remote CRM.
dispatch(new UpdateStage($activity, $lead, $callStage, $stage));
$this->logger->info(
sprintf(
'[%s] User changing lead stage from %s to %s',
$crmService->getDisplayName(),
$callStage->getName(),
$stage->getName()
),
[
'user' => $user->getUuid(),
'lead' => $lead->getUuid(),
]
);
} else {
$this->logger->info('Stage has not changed');
// Stage remains as current.
$callStage = $stage;
}
break;
case 'account':
$this->logger->info('Processing account');
// If the object is not a lead, it should be an account.
$account = $team->crm->accounts()->where('crm_provider_id', $objectId)->first();
// Account does not exist locally, import it.
if ($account === null) {
$this->logger->info('Account does not exist locally');
$account = $crmService->syncAccount($objectId);
}
$this->logger->info('Account found', ['accountId' => $account->id]);
break;
case 'contact':
$this->logger->info('processing contact');
$contact = $team->crm->contacts()->where('crm_provider_id', $objectId)->first();
// Contact does not exist locally, import it.
if (! $contact instanceof Contact) {
$this->logger->info('contact does not exist locally');
$contact = $crmService->syncContact($objectId);
}
$this->logger->info('resolving account');
$account = $this->resolveAccount($team, $contact, $crmService, $prospects);
break;
}
// If they have specified an opportunity, retrieve this with stage.
if ($opportunityId) {
$this->logger->info('opportunity id is set');
$opportunity = $team->crm->opportunities()->where('crm_provider_id', $opportunityId)->first();
// Opportunity does not exist locally, import it.
if ($opportunity === null) {
$this->logger->info('opportunity does not exist locally');
$opportunity = $crmService->syncOpportunity($opportunityId);
}
if ($stageId === null) {
$this->logger->info('stage id is null');
// If it was not provided, just assume it is the current stage.
$callStage = $opportunity->stage ?? null;
} else {
$this->logger->info('looking for stage');
/** @var ?Stage $opportunityStage */
$opportunityStage = $team->crm
->stages()
->uuid($stageId, false)
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
// There is a chance we still cannot import this opportunity.
if ($opportunityStage !== null && $opportunity !== null && $opportunity->stage_id !== $opportunityStage->id) {
$this->logger->info('opportunity stage has changed');
// Storage current stage on activity.
$callStage = $opportunity->stage;
dispatch(new UpdateStage($activity, $opportunity, $callStage, $opportunityStage));
$this->logger->info(
sprintf(
'[%s] User changing opportunity stage from %s to %s',
$crmService->getDisplayName(),
$callStage->name,
$opportunityStage->name
),
[
'userId' => $user->id_string,
'opportunityId' => $opportunity->id_string,
]
);
} else {
$this->logger->info('opportunity stage has not changed');
// Stage remains as current.
$callStage = $opportunityStage;
}
}
}
if ($crmProviderId) {
// Cast $crmProviderId to string otherwise it won't use database index for some records
$linkedActivity = Activity::where('crm_provider_id', (string) $crmProviderId)->first();
// Check if this activity has already been assigned to a different activity.
if ($linkedActivity && $linkedActivity->id !== $activity->id) {
throw new InvalidArgumentException(
'Sorry, the linked task has already been logged under a different call. '
. 'Please choose another linked task.'
);
}
}
} catch (InvalidArgumentException $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorWrongArgs($exception->getMessage());
} catch (Exception $exception) {
$this->logger->error('Failed to process prospect', [
'prospect_data' => $prospectData,
'reason' => $exception->getMessage(),
]);
// Return a JSON response with the response array and status code.
return $this->response->errorInternalError(
'Sorry, an error occurred. Please try again or reach out to support if the problem continues.'
);
}
}
if ($categoryId) {
$category = PlaybookCategory::uuid($categoryId);
if ($category->playbook->team_id !== $team->id) {
throw new InvalidArgumentException('Sorry, this category does not belong to your playbook.');
}
$activity->playbook_category_id = $category->id;
}
$this->logger->info('Prospect data', [
'lead_id' => $lead?->getId(),
'account_id' => $account?->getId(),
'contact_id' => $contact?->getId(),
'opportunity_id' => $opportunity?->getId(),
'stage_id' => $stage?->getId(),
]);
if ($title) {
$activity->title = $title;
}
if ($summary) {
$activity->summary = $summary;
}
if ($crmProviderId) {
$activity->crm_provider_id = $crmProviderId;
}
if ($callStage) {
$this->logger->info('Setting stage id', ['stageId' => $callStage->id]);
$activity->stage_id = $callStage->id;
}
if ($lead) {
$this->logger->info('Setting lead id', ['leadId' => $lead->id]);
$activity->lead_id = $lead->id;
// If we are changed from an account > lead, unset the account data.
$this->logger->info('Unsetting account id, opportunity id, contact id, value');
$activity->account_id = null;
$activity->opportunity_id = null;
$activity->contact_id = null;
$activity->value = null;
}
if ($account) {
$this->logger->info('Setting account id', ['accountId' => $account->id]);
$activity->account_id = $account->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('unsetting lead id');
$activity->lead_id = null;
// Unset the contact if switching different accounts. Will be set up below if still applicable.
if (! $team->hasFeature(FeatureEnum::LINK_ACTIVITY_TO_MULTIPLE_PROSPECTS) || empty($contact)) {
$this->logger->info('Unsetting contact id');
$activity->contact_id = null;
}
}
if ($opportunity) {
$this->logger->info('setting opportunity id', ['opportunityId' => $opportunity->id]);
$this->logger->info('unsetting lead id');
$activity->opportunity_id = $opportunity->id;
$activity->value = $opportunity->value;
// If we are changed from an lead > account, unset the lead data.
$activity->lead_id = null;
}
if ($contact) {
$this->logger->info('setting contact id', ['contactId' => $contact->id]);
$activity->contact_id = $contact->id;
// If we are changed from an lead > account, unset the lead data.
$this->logger->info('Unsetting lead id');
$activity->lead_id = null;
}
$activity->is_internal = $isInternal;
$activity->save();
$activity->refresh();
$this->logger->notice('Activity saved', [
'activity_id' => $activity->getId(),
'lead_id' => $activity->lead_id,
'account_id' => $activity->account_id,
'contact_id' => $activity->contact_id,
'opportunity_id' => $activity->opportunity_id,
'stage_id' => $activity->stage_id,
'crm_provider_id' => $activity->getCrmProviderId(),
]);
// Store entities as field data on the activity.
$updatedData = $this->storeEntities($crmService, $activity, $layout, $rawEntities);
if ($activity->isLoggable()) {
// Follow-up Task or Event data.
$followupData = $this->fetchFollowupEntities($crmService, $layout, $rawEntities);
$this->logger->info('CRM LOG manual log triggered', [
'activityId' => $activity->getUuid(),
'followupData' => $followupData,
'userId' => $user->getUuid(),
]);
// Store data in the CRM.
// ++add check for crm_required
$job = new SaveActivity($activity, $followupData);
if ($updatedData) {
$job->delay(Carbon::now()->addMinutes($jobDelay));
}
dispatch($job);
// Manually dispatch log for Opportunity or Prospect added
if ($activity->hasOpportunity() || $activity->hasProspect()) {
event(new ActivityProspectAdded(
activity: $activity,
eventSource: 'manually-log-crm-data'
));
}
}
return $this->response->withOk();
}
/**
* Extract any activity data to be upserted in the Lead/Opportunity/Task etc in the CRM.
*
* @param ServiceInterface $service
* @param Activity $activity
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function storeEntities(ServiceInterface $service, Activity $activity, Layout $layout, array $entities): array
{
$updatedData = [];
$existingData = $activity->data()->get();
// We need to delete any existing data to overwrite with latest values.
$activity->data()->delete();
$layoutEntities = $layout->entities()
->with('field', 'parent')
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->get();
/** @var LayoutEntity $entity */
foreach ($layoutEntities as $entity) {
// If the user has provided a value for this entity
if (array_key_exists($entity->id_string, $entities)) {
$value = $entities[$entity->id_string];
// Convert raw data into values that the CRM can consume.
if ($value) {
$value = $service->normalizeValue($entity->field->type, $value);
}
// Check the field is part of the activity-summary section.
if ($entity->parent && $entity->parent->label === 'activity-summary' && $value) {
// This is the internal database ID, not the external CRM ID.
$objectId = null;
switch ($entity->field->object_type) {
case Field::OBJECT_ACCOUNT:
$objectId = $activity->account_id;
break;
case Field::OBJECT_CONTACT:
$objectId = $activity->contact_id;
break;
case Field::OBJECT_OPPORTUNITY:
$objectId = $activity->opportunity_id;
break;
case Field::OBJECT_LEAD:
$objectId = $activity->lead_id;
break;
case Field::OBJECT_TASK:
case Field::OBJECT_EVENT:
$objectId = $activity->id;
break;
}
if ($objectId) {
/** @var FieldData $data */
$data = $activity->data()->create([
'crm_layout_entity_id' => $entity->id,
'crm_field_id' => $entity->crm_field_id,
'object_type' => $entity->field->object_type,
'object_id' => $objectId,
'value' => $value,
]);
// Never send read-only field data to the CRM.
if ($entity->read_only === false && $entity->is_visible) {
$existingValue = $existingData
->where('crm_layout_entity_id', $entity->id)
->where('crm_field_id', $entity->crm_field_id)
->where('object_type', $entity->field->object_type)
->where('object_id', $objectId)
->first();
// If the field was actually changed, we need to reflect this in the CRM too.
if ($existingValue === null || $existingValue->value !== $value) {
$updatedData[] = $data->id;
}
}
}
}
}
}
return $updatedData;
}
/**
* Extract any followup data to be dispatched in a job to create a new Task/Event in the CRM.
*
* @param ServiceInterface $crmService
* @param Layout $layout
* @param array $entities The raw entity data from user
*
* @return array
*/
private function fetchFollowupEntities(ServiceInterface $crmService, Layout $layout, array $entities): array
{
$fieldData = [];
foreach ($entities as $entityId => $value) {
// Only bother with fields that have a value.
if ($value) {
// Extract the entity from the UUID. Check the field is valid and part of the follow-up section.
$entity = $layout->entities()
->uuid($entityId, false)
->whereHas('parent', function ($query) {
$query->where('label', 'follow-up');
})
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->first();
if ($entity) {
// Convert raw data into values that the CRM can consume.
$value = $crmService->normalizeValue($entity->field->type, $value);
// Add the field and value to the payload.
$fieldData += [
$entity->field->crm_provider_id => $value,
];
}
}
}
return $fieldData;
}
/**
* @param Activity $activity
*/
private function validateSummary(Activity $activity): void
{
$team = $activity->user->team;
$crmProvider = $team->crm->provider;
$attributes = [];
$rules = [
'layout_id' => 'required|uuid:crm_layouts,crm_configuration_id,' . $team->crm_id,
'title' => 'string|max:250',
'prospects' => 'required|array',
'opportunity_id' => new CrmReference($crmProvider),
'category_id' => 'uuid:playbook_categories|required_unless:is_internal,true',
'stage_id' => 'uuid:stages,team_id,' . $team->id, // Todo: move to proper validator
'summary' => 'max:50000',
'nId' => 'exists:notifications,id',
'crm_id' => new CrmReference($crmProvider),
'entities' => 'array',
'is_internal' => 'boolean',
];
/** @var Layout $layout */
$layout = $team->crm->layouts()->uuid($this->request->input('layout_id'));
// Only validate fields, not headers etc. If not loggable, we don't care about follow-up section.
$entities = $layout->entities()
->where('read_only', 0)
->whereHas('field', function ($query) {
$query->where('is_selectable', 1);
})
->whereHas('parent', function ($query) use ($activity) {
if ($activity->isLoggable() === false) {
$query->where('label', '<>', 'follow-up');
}
});
$isInternal = $this->request->input('is_internal', false);
foreach ($entities->get() as $entity) {
$rules += $this->buildFieldValidator($entity, $isInternal);
$attributes += $this->buildFieldMessage($entity);
}
$this->request->validate($rules, [], $attributes);
}
private function buildFieldValidator(LayoutEntity $entity, bool $isInternal): array
{
return [
'entities.' . $entity->id_string => $entity->getValidator($isInternal),
];
}
/**
* @param LayoutEntity $entity
*
* @return array
*/
private function buildFieldMessage(LayoutEntity $entity): array
{
$label = $entity->label;
if ($label === null) {
$label = $entity->field->label;
}
return [
'entities.' . $entity->id_string => $label,
];
}
public function search(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->debugLog(
$user,
'User extracted from request',
['user' => $user->getId(), 'tz' => $user->getTimezone()]
);
$searchCriteria = Criteria::createFromRequest($request->all(), $user->getTimezone());
$this->debugLog(
$user,
'ActivitySearch criteria built',
['searchCriteria' => $searchCriteria]
);
$filterSet = $this->activitySearch->getHomepageFilterSet($searchCriteria, $user);
$this->debugLog($user, 'FilterSet built', ['filterSet' => $filterSet]);
$this->validateSearch($request, $filterSet);
$this->debugLog($user, 'Request validated');
$searchResponse = $repository->onDemandSearch($user, $searchCriteria, $filterSet);
/** @var Collection<Activity> $activities */
$activities = $searchResponse['results'];
$this->debugLog($user, 'Activities ES response extracted');
$hideInternalMeetingsSetting = $this->teamRepository->getTeamSettingByTeamId(
$user->getTeamId(),
TeamSetting::HIDE_INTERNAL_SCHEDULED_MEETINGS->name(),
);
if ($hideInternalMeetingsSetting?->getValue() === '1') {
$activities = $activities->filter(function (Activity $activity) {
if ($activity->is_internal && empty($activity->actual_start_time)) {
return false;
}
return true;
});
}
$this->debugLog($user, 'Internal meetings (?!) filtered');
$this->response->getManager()
->parseIncludes([
'category',
'organizer.group',
'prospect',
'stage',
'opportunity',
'stats',
'scorecards',
'masterTrack',
'activeParticipants',
'notification',
])
->setSerializer(new JsonSerializer());
$transformerExcludes = $this->request->input('exclude');
if ($transformerExcludes) {
$this->response->getManager()->parseExcludes($transformerExcludes);
}
$this->debugLog($user, 'Response Manager (?!) applied');
$transformer = new ActivityTransformer();
$transformer->setConsumer($user);
$this->debugLog($user, 'Activity Transformer added');
$resource = new \League\Fractal\Resource\Collection($activities, $transformer);
$page = $searchCriteria->getPageNumber();
$this->debugLog($user, 'Search criteria page number called', ['page' => $page]);
$histogram = array_pluck(array_get($searchResponse, 'histogram.buckets', []), 'doc_count', 'key_as_string');
$this->debugLog($user, 'Histogram generated. Response is ready.', ['histogram' => $histogram]);
return $this->response->withArray([
'pagination' => [
'total' => $searchResponse['totalHits'],
'current' => $page,
'prev' => max($page - 1, 1),
'next' => $page + 1,
],
'results' => $this->response->getManager()->createData($resource)->toArray(),
'histogram' => $histogram,
]);
}
private function debugLog(User $user, string $logMessage, ?array $context = []): void
{
// Debug for Learning People Only
if ($user->getTeamId() !== 260) {
return;
}
Log::notice(
sprintf('[activity-search-controller] %s', $logMessage),
$context
);
}
/** @throws ValidationException */
private function validateSearch(Request $request, FilterDefinitionCollection $filterSet, ?string $prefix = null): void
{
$rules = [
'exclude' => 'array',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
];
if ($prefix !== null && mb_strpos($prefix, '.') !== false) {
$rules[rtrim($prefix, '.')] = sprintf(
'required|array|max:%d',
$filterSet->count()
);
}
$validationRules = $filterSet->getValidationRules($prefix)
->merge($rules)
->all();
$request->validate($validationRules);
}
public function createActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$search = $this->updateOrCreateActivitySearch($request);
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function updateActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('update', $search);
$this->updateOrCreateActivitySearch($request, $search);
return $this->response->withOk();
}
private function storeNamedSearchFilters(
Collection $request,
Search $search,
FilterDefinitionCollection $filterSet,
?string $prefix = null,
): self {
$arrayTypeProperties = $filterSet
->getPropertyTypes([
FilterDefinitionCollection::PROPERTY_TYPE_ARRAY,
])
->all();
$supportedRequestProperties = $filterSet->getSupportedRequestProperties($prefix);
foreach ($supportedRequestProperties as $requestPropertyName) {
if (! array_has($request, $requestPropertyName)) {
continue;
}
/** @var string|string[] $propertyValue */
$propertyValue = array_get($request, $requestPropertyName);
$propertyName = $prefix === null
? $requestPropertyName
: mb_substr($requestPropertyName, mb_strlen($prefix));
$isArrayType = array_has($arrayTypeProperties, $propertyName);
if (! $isArrayType) {
/** @var string $requestPropertyValue */
$search->filters()->updateOrCreate(
[
'filter' => $propertyName,
],
[
'value' => $propertyValue,
]
);
continue;
}
/** @var string[] $requestPropertyValue */
/** @var SearchFilter[]|Collection $existingFilterValues */
$existingFilterValuesKeyed = $search->filters()
->where('filter', $propertyName)
->get()
->keyBy('id');
// Iterate over values provided as request parameters
foreach ($propertyValue as $value) {
/** @var SearchFilter|null $valueFilter */
$valueFilter = $search->filters()
->where(
[
'filter' => $propertyName,
'value' => $value,
]
)
->first();
if ($valueFilter !== null) {
// Remove filter value pair from list to be deleted
$existingFilterValuesKeyed->forget($valueFilter->id);
} else {
// Add new filter/value pair
$search->filters()->updateOrCreate([
'filter' => $propertyName,
'value' => $value,
]);
}
}
// Delete filter value pairs for this filter that no longer exist in request parameters
foreach ($existingFilterValuesKeyed as $existingFilter) {
$existingFilter->delete();
}
}
/** @var Collection<int, SearchFilter> $filtersKeyed */
$filtersKeyed = $search->filters()->get()->keyBy('filter');
// wipe removed filters from this search
foreach ($filtersKeyed as $filterName => $filter) {
if (array_has($request, $prefix . $filterName)) {
continue;
}
// Remove all filter values for this filter
$search->filters()->where('filter', $filterName)->delete();
}
return $this;
}
/**
* @throws AuthorizationException
*/
public function fetchActivitySearch(
Search $search,
Request $request,
SearchTransformer $searchTransformer,
): JsonResponse {
$this->authorize('view', $search);
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
$search->loadCount('automatedReports');
return $this->response->withItem(
$search,
$searchTransformer
->withConsumer($user)
);
}
public function listActivitySearch(Request $request, SearchTransformer $searchTransformer): JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->response
->getManager()
->setSerializer(new JsonSerializer());
return $this->response->withCollection(
$user->searches()->withCount('automatedReports')->get(),
$searchTransformer
->withConsumer($user)
);
}
/**
* Deletes a saved search
*
* @param Request $request
* @param Search $search
*
* @throws Exception
*
* @return JsonResponse
*/
public function deleteActivitySearch(Request $request, Search $search): JsonResponse
{
$this->authorize('delete', $search);
// Disable and orphan any AutomatedReports that use this search
$search->automatedReports()->withTrashed()->update([
'activity_search_id' => null,
'status' => false,
]);
// Delete filters and the search itself
$search->filters()->delete();
$search->delete();
return $this->response->withOk();
}
public function live(Request $request, ElasticActivityRepository $repository): JsonResponse
{
$user = $this->getUserFromRequest($request);
$this->request->validate([
'sort_direction' => 'in:asc,desc',
'limit' => 'integer|min:1|max:50',
'page' => 'integer|min:1',
]);
$activities = $repository->getLiveCoachingEligibleActivities(
user: $user,
lookBackMinutes: self::LOOK_BACK,
limit: (int) $this->request->input('limit', 25),
page: (int) $this->request->input('page', 1),
sortBy: ['actual_start_time', 'scheduled_start_time'],
sortDirection: (string) $this->request->input('sort_direction', 'asc'),
);
$this->response
->getManager()
->parseIncludes(['organizer.group', 'prospect'])
->setSerializer(new JsonSerializer());
return $this->response->withCollection($activities, new ActivityTransformer());
}
/**
* @param Activity $activity
*
* @throws AuthorizationException
*
* @return mixed
*/
public function show(Activity $activity, ActivityService $activityService): JsonResponse
{
$this->authorize('show', $activity);
$user = $activity->getUser();
$team = $user->getTeam();
// Sync the opportunity with the latest data if possible.
if ($activity->opportunity_id) {
try {
$crmService = $this->providerRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($team->getOwner());
} else {
$crmService->setUser($user);
}
$crmService->syncOpportunity($activity->opportunity->crm_provider_id);
} catch (Exception $exception) {
// Move on.
}
}
$activityData = $activityService->getActivityData($this->request->user(), $activity);
return response()->json($activityData);
}
public function createRecording(Activity $activity)
{
$this->authorize('record', $activity);
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Tell Twilio to start recording this activity.
if ($activity->recording_state === Activity::RECORDING_OFF) {
$job = (new StartRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withCreated();
}
return $this->response->errorGone('Activity is already recording.');
}
public function updateRecording(Request $request, Activity $activity)
{
$this->authorize('record', $activity);
$request->validate([
'preference' => 'boolean',
'state' => [
'string',
Rule::in([
Activity::RECORDING_IN_PROGRESS,
Activity::RECORDING_PAUSED,
]),
],
]);
if ($request->has('state')) {
if ($activity->hasRecordingReasonComplianceRestricted()) {
return $this->response->errorGone('Recording this number has been disabled by your organization.');
}
// Toggle the recording state between paused and resumed.
if (! $activity->isRecordingState(Activity::RECORDING_OFF)) {
$job = (new ToggleRecording($activity, $request->input('state')))
->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Recording is not toggleable.');
}
if ($request->has('preference')) {
$activity->update([
'recording_preference' => $request->input('preference') ? 1 : 0,
]);
return $this->response->withOk();
}
return $this->response->errorWrongArgs('Something went wrong');
}
public function stopRecording(Activity $activity)
{
$this->authorize('stopRecord', $activity);
// Tell Twilio to stop recording this activity.
if ($activity->isRecordingState(Activity::RECORDING_IN_PROGRESS)) {
$job = (new StopRecording($activity))->onQueue(Constants::QUEUE_CONFERENCES);
dispatch($job);
return $this->response->withOk();
}
return $this->response->errorGone('Activity is not recording.');
}
/**
* Add activity to this user's favorites playlist
*
* @throws AuthorizationException
*/
public function favorite(Activity $activity, PlaylistActivityRepository $playlistActivityRepository): JsonResponse
{
$this->authorize('favorite', $activity);
$user = $this->getUserFromRequest($this->request);
$favorite = $activity->wasFavoritedBy($user);
$name = $activity->activity_title ?? '';
// It needs to check at least one record.
if (! $favorite) {
$favoritePlaylist = $user->favoritePlaylist();
$playlistActivity = $playlistActivityRepository->findByBaseActivityUserAndPlaylist(
$activity,
$user,
$favoritePlaylist
);
if ($playlistActivity !== null) {
$playlistActivity->update(
// Just update, don't sort.
['start_time' => 0, 'name' => mb_strimwidth($name, 0, 100)],
);
} else {
$playlistActivity = $activity->playlistActivities()->create([
'playlist_id' => $favoritePlaylist->getId(),
'user_id' => $user->getId(),
'start_time' => 0,
'name' => mb_strimwidth($name, 0, 100),
]);
// Sort it on top.
$playlistActivity->update(
[
'sort' => $playlistActivityRepository->calculateNewSortOrder(
null,
$playlistActivity,
),
],
);
}
$playlistActivityRepository->calculateNewSortOrder(null, $playlistActivity);
return new JsonResponse([], JsonResponse::HTTP_CREATED);
}
return new JsonResponse(
[
'error' => [
'code' => AbstractResponse::CODE_CONFLICT,
'http_code' => JsonResponse::HTTP_CONFLICT,
'message' => 'Resource Already Exists',
],
],
JsonResponse::HTTP_CONFLICT,
);
}
/**
* Remove activity from this user's favorites playlist
*
* @param Activity $activity
*
* @throws AuthorizationException
*
...
|
NULL
|
NULL
|
NULL
|
NULL
|