|
7590
|
340
|
7
|
2026-05-08T09:11:12.603608+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778231472603_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
4 files, folder partially checked
app 2 files, folder checked
Component/Datadog 1 file, folder checked
Constants.php, final class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackService.php, class checked
tests/Unit/Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackServiceTest.php, class checked
.env.local not checked
app 2 files, folder checked
Component/Datadog 1 file, folder checked
Constants.php, final class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackService.php, class checked
Component/Datadog 1 file, folder checked
Constants.php, final class checked
Constants.php, final class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackService.php, class checked
AutomatedReportsCallbackService.php, class checked
tests/Unit/Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackServiceTest.php, class checked
AutomatedReportsCallbackServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
3 modified
hubspot rate limit temp checkout
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers...
|
[{"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":"4 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.15083799,"width":0.030585106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.16839585,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/Datadog 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.07014628,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php, final class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.045877658,"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.22106944,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.096409574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Kiosk/AutomatedReports 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.25618514,"width":0.119015954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.273743,"width":0.10538564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.16839585,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/Datadog 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.07014628,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php, final class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.045877658,"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.22106944,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.096409574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component/Datadog 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.07014628,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php, final class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.045877658,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php, final class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.045877658,"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.22106944,"width":0.09740692,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.096409574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackService.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.096409574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Kiosk/AutomatedReports 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.25618514,"width":0.119015954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.273743,"width":0.10538564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCallbackServiceTest.php, class checked","depth":4,"bounds":{"left":0.27726063,"top":0.273743,"width":0.10538564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.29130086,"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.30885875,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"hubspot rate limit temp checkout","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"hubspot rate limit temp checkout","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5660418648173127938
|
-417998989627161252
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
4 files, folder partially checked
app 2 files, folder checked
Component/Datadog 1 file, folder checked
Constants.php, final class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackService.php, class checked
tests/Unit/Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackServiceTest.php, class checked
.env.local not checked
app 2 files, folder checked
Component/Datadog 1 file, folder checked
Constants.php, final class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackService.php, class checked
Component/Datadog 1 file, folder checked
Constants.php, final class checked
Constants.php, final class checked
Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackService.php, class checked
AutomatedReportsCallbackService.php, class checked
tests/Unit/Services/Kiosk/AutomatedReports 1 file, folder checked
AutomatedReportsCallbackServiceTest.php, class checked
AutomatedReportsCallbackServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
3 modified
hubspot rate limit temp checkout
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers...
|
7583
|
NULL
|
NULL
|
NULL
|
|
20092
|
864
|
6
|
2026-05-11T14:29:50.971624+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778509790971_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
4 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not 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":"4 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Console/Commands 1 file, folder not checked","depth":5,"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/Services/Crm/Hubspot 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":6,"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/Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Crm/Hubspot 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"on_screen":true,"role_description":"text"}]...
|
502912026519174925
|
-4426923907685014260
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
4 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked...
|
20091
|
NULL
|
NULL
|
NULL
|
|
20091
|
864
|
5
|
2026-05-11T14:29:50.404130+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778509790404_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
4 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
1 modified
JY-20725 add tests
Commit Message
Commit Message History
Git
Author:
Author:...
|
[{"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":"4 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Console/Commands 1 file, folder not checked","depth":5,"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/Services/Crm/Hubspot 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":6,"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/Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Crm/Hubspot 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unversioned Files 9 files not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 add tests","depth":2,"on_screen":true,"value":"JY-20725 add tests","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7294065924032325812
|
-4354866243853868020
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
4 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
1 modified
JY-20725 add tests
Commit Message
Commit Message History
Git
Author:
Author:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20094
|
865
|
5
|
2026-05-11T14:29:51.638299+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778509791638_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
4 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
1 modified
JY-20725 add tests
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
6 differences
cbed2c9a
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willRetu...
|
[{"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":"4 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.030585106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.078125,"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.16839585,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Crm/Hubspot 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.1859537,"width":0.095744684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.20351157,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.078125,"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.16839585,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Crm/Hubspot 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.1859537,"width":0.095744684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.20351157,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":4,"bounds":{"left":0.27726063,"top":0.20351157,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.22106944,"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.2386273,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.1931516,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 add tests","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 add tests","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"6 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"cbed2c9a","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.023603724,"height":0.013567438},"on_screen":true,"value":"cbed2c9a","help_text":"text/plain","role_description":"text field","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.25731382,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4398271,"top":0.43415803,"width":0.13863032,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:portal:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests\n // -------------------------------------------------------------------------\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->client->isHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests\n // -------------------------------------------------------------------------\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->client->parseRetryAfter($e));\n }\n}","depth":2,"bounds":{"left":0.44448137,"top":0.42697525,"width":0.27194148,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:portal:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests\n // -------------------------------------------------------------------------\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->client->isHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->client->isHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests\n // -------------------------------------------------------------------------\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->client->parseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->client->parseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-4114485583209227198
|
1852355439196940796
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
4 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit/Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
1 modified
JY-20725 add tests
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
6 differences
cbed2c9a
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willRetu...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
19630
|
839
|
27
|
2026-05-11T13:11:35.990487+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778505095990_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmDataTest.php, class checked
Services/Crm/Hubspot/Pagination 1 file, folder checked
HubspotPaginationServiceTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmDataTest.php, class checked
Services/Crm/Hubspot/Pagination 1 file, folder checked
HubspotPaginationServiceTest.php, class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmDataTest.php, class checked
MatchActivityCrmDataTest.php, class checked
Services/Crm/Hubspot/Pagination 1 file, folder checked
HubspotPaginationServiceTest.php, class checked
HubspotPaginationServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
2 modified
JY-20725 add HS rate limit handling on activities rematching
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
44d5678c
<?php
declare(strict_types=1);
namespace Tests\Unit\Jobs\Crm;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Jobs\Crm\MatchActivityCrmData;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ActivityRepository;
use Jiminny\Services\Crm\CrmActivityService;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class MatchActivityCrmDataTest extends TestCase
{
private ActivityRepository|MockObject $activityRepository;
private CrmActivityService|MockObject $crmActivityService;
private Connection|MockObject $connection;
protected function setUp(): void
{
parent::setUp();
$this->activityRepository = $this->createMock(ActivityRepository::class);
$this->crmActivityService = $this->createMock(CrmActivityService::class);
$this->connection = $this->createMock(Connection::class);
}
/**
* @throws \Exception|Exception
*/
public function testHandle(): void
{
$fromConfiguration = $this->createMock(Configuration::class);
$fromConfiguration->method('getId')->willReturn(5);
$team = $this->createMock(Team::class);
$user = $this->createMock(User::class);
$user->method('getTeam')->willReturn($team);
$activityConfiguration = $this->createMock(Configuration::class);
$activityConfiguration->method('getId')->willReturn(10);
$participants = $this->createMock(HasMany::class);
$activity = $this->createMock(Activity::class);
$activity->method('getUuid')->willReturn('test');
$activity->method('getProvider')->willReturn('provider');
$activity->method('getUser')->willReturn($user);
$activity->method('getCrm')->willReturn($activityConfiguration);
$activity->method('participants')->willReturn($participants);
$activity->expects($this->exactly(2))->method('update');
$this->activityRepository->expects(($this->once()))
->method('findById')
->with(1)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: $fromConfiguration,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
/**
* @throws \Exception|Exception
*/
public function testHandleNoActivity(): void
{
$this->activityRepository->expects(($this->once()))
->method('findById')
->with(1)
->willReturn(null);
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: null,
);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('[MatchActivityCrmData] Cannot find activity.');
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testSkipsCrmConfigurationSwitchWhenAlreadyMatches(): void
{
$fromConfiguration = $this->createMock(Configuration::class);
$fromConfiguration->method('getId')->willReturn(5);
$activityConfiguration = $this->createMock(Configuration::class);
$activityConfiguration->method('getId')->willReturn(5);
$participants = $this->createMock(HasMany::class);
$activity = $this->createMock(Activity::class);
$activity->method('getCrm')->willReturn($activityConfiguration);
$activity->method('participants')->willReturn($participants);
$activity->expects($this->once())->method('update');
$this->activityRepository->expects($this->once())
->method('findById')
->with(123)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: $fromConfiguration,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testJobConfiguration(): void
{
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: null,
remoteSearch: false,
);
$this->assertSame(3, $job->tries);
$this->assertSame(300, $job->timeout());
$this->assertSame(360, $job->uniqueFor());
$this->assertSame([30, 90, 180], $job->backoff());
}
public function testUniqueId(): void
{
$configuration = $this->createMock(Configuration::class);
$configuration->method('getId')->willReturn(5);
$job1 = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: false,
);
$this->assertSame('123:0:local', $job1->uniqueId());
$job2 = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: true,
);
$this->assertSame('123:0:remote', $job2->uniqueId());
$job3 = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: $configuration,
remoteSearch: false,
);
$this->assertSame('123:5:local', $job3->uniqueId());
}
public function testTransactionRollbackOnException(): void
{
$activity = $this->createMock(Activity::class);
$this->activityRepository->expects($this->once())
->method('findById')
->with(1)
->willReturn($activity);
$exception = new \RuntimeException('Test exception');
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) use ($exception) {
throw $exception;
});
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: null,
);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Test exception');
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testFailedMethodLogsError(): void
{
$exception = new \RuntimeException('Test failure');
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: true,
);
$job->failed($exception);
$this->assertTrue(true);
}
public function testLogsSuccessWhenMatchFound(): void
{
$participants = $this->createMock(HasMany::class);
$lead = $this->createMock(Lead::class);
$lead->method('getId')->willReturn(456);
$activity = $this->createMock(Activity::class);
$activity->method('participants')->willReturn($participants);
$activity->method('getLead')->willReturn($lead);
$activity->method('getContact')->willReturn(null);
$activity->method('getAccount')->willReturn(null);
$activity->method('getOpportunity')->willReturn(null);
$this->activityRepository->expects($this->once())
->method('findById')
->with(123)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: false,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testLogsNoMatchWhenNoMatchFound(): void
{
$participants = $this->createMock(HasMany::class);
$activity = $this->createMock(Activity::class);
$activity->method('participants')->willReturn($participants);
$activity->method('getLead')->willReturn(null);
$activity->method('getContact')->willReturn(null);
$activity->method('getAccount')->willReturn(null);
$activity->method('getOpportunity')->willReturn(null);
$this->activityRepository->expects($this->once())
->method('findById')
->with(123)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, true);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: true,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testLogsOldStateBeforeClearing(): void
{
$participant1 = new \stdClass();
$participant1->id = 10;
$participant1->user_id = 100;
$participant1->contact_id = 200;
$participant1->lead_id = null;
$participant2 = new \stdClass();
$participant2->id = 20;
$participant2->user_id = null;
$participant2->contact_id = null;
$participant2->lead_id = 300;
$participantsCollection = collect([$participant1, $participant2]);
$participantsRelation = $this->createMock(HasMany::class);
$lead = $this->createMock(Lead::class);
$lead->method('getId')->willReturn(111);
$contact = $this->createMock(Contact::class);
$contact->method('getId')->willReturn(222);
$account = $this->createMock(Account::class);
$account->method('getId')->willReturn(333);
$opportunity = $this->createMock(Opportunity::class);
$opportunity->method('getId')->willReturn(444);
$stage = $this->createMock(Stage::class);
$stage->method('getId')->willReturn(555);
$activity = $this->createMock(Activity::class);
$activity->method('participants')->willReturn($participantsRelation);
$activity->method('getLead')->willReturn($lead);
$activity->method('getContact')->willReturn($contact);
$activity->method('getAccount')->willReturn($account);
$activity->method('getOpportunity')->willReturn($opportunity);
$activity->method('getStage')->willReturn($stage);
$this->activityRepository->expects($this->once())
->method('findById')
->with(999)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn($participantsCollection);
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 999,
fromConfiguration: null,
remoteSearch: false,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
}...
|
[{"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":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.11572227,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.13328013,"width":0.078125,"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.15083799,"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.16839585,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmDataTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08045213,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot/Pagination 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.09707447,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HubspotPaginationServiceTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.08843085,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.25618514,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.13328013,"width":0.078125,"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.15083799,"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.16839585,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmDataTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08045213,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot/Pagination 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.09707447,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HubspotPaginationServiceTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.08843085,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmDataTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08045213,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmDataTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.08045213,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot/Pagination 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.09707447,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HubspotPaginationServiceTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.08843085,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HubspotPaginationServiceTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.08843085,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.25618514,"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.273743,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 add HS rate limit handling on activities rematching","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 add HS rate limit handling on activities rematching","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"4 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"44d5678c","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.023936171,"height":0.013567438},"on_screen":true,"value":"44d5678c","help_text":"text/plain","role_description":"text field","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\\Jobs\\Crm;\n\nuse Illuminate\\Database\\Connection;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Jobs\\Crm\\MatchActivityCrmData;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActivityRepository;\nuse Jiminny\\Services\\Crm\\CrmActivityService;\nuse PHPUnit\\Framework\\MockObject\\Exception;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass MatchActivityCrmDataTest extends TestCase\n{\n private ActivityRepository|MockObject $activityRepository;\n private CrmActivityService|MockObject $crmActivityService;\n private Connection|MockObject $connection;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n $this->activityRepository = $this->createMock(ActivityRepository::class);\n $this->crmActivityService = $this->createMock(CrmActivityService::class);\n $this->connection = $this->createMock(Connection::class);\n }\n\n /**\n * @throws \\Exception|Exception\n */\n public function testHandle(): void\n {\n $fromConfiguration = $this->createMock(Configuration::class);\n $fromConfiguration->method('getId')->willReturn(5);\n\n $team = $this->createMock(Team::class);\n\n $user = $this->createMock(User::class);\n $user->method('getTeam')->willReturn($team);\n\n $activityConfiguration = $this->createMock(Configuration::class);\n $activityConfiguration->method('getId')->willReturn(10);\n\n $participants = $this->createMock(HasMany::class);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getUuid')->willReturn('test');\n $activity->method('getProvider')->willReturn('provider');\n $activity->method('getUser')->willReturn($user);\n $activity->method('getCrm')->willReturn($activityConfiguration);\n $activity->method('participants')->willReturn($participants);\n $activity->expects($this->exactly(2))->method('update');\n\n $this->activityRepository->expects(($this->once()))\n ->method('findById')\n ->with(1)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: $fromConfiguration,\n );\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n /**\n * @throws \\Exception|Exception\n */\n public function testHandleNoActivity(): void\n {\n $this->activityRepository->expects(($this->once()))\n ->method('findById')\n ->with(1)\n ->willReturn(null);\n\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: null,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('[MatchActivityCrmData] Cannot find activity.');\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testSkipsCrmConfigurationSwitchWhenAlreadyMatches(): void\n {\n $fromConfiguration = $this->createMock(Configuration::class);\n $fromConfiguration->method('getId')->willReturn(5);\n\n $activityConfiguration = $this->createMock(Configuration::class);\n $activityConfiguration->method('getId')->willReturn(5);\n\n $participants = $this->createMock(HasMany::class);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrm')->willReturn($activityConfiguration);\n $activity->method('participants')->willReturn($participants);\n $activity->expects($this->once())->method('update');\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(123)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: $fromConfiguration,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testJobConfiguration(): void\n {\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: null,\n remoteSearch: false,\n );\n\n $this->assertSame(3, $job->tries);\n $this->assertSame(300, $job->timeout());\n $this->assertSame(360, $job->uniqueFor());\n $this->assertSame([30, 90, 180], $job->backoff());\n }\n\n public function testUniqueId(): void\n {\n $configuration = $this->createMock(Configuration::class);\n $configuration->method('getId')->willReturn(5);\n\n $job1 = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: false,\n );\n $this->assertSame('123:0:local', $job1->uniqueId());\n\n $job2 = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: true,\n );\n $this->assertSame('123:0:remote', $job2->uniqueId());\n\n $job3 = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: $configuration,\n remoteSearch: false,\n );\n $this->assertSame('123:5:local', $job3->uniqueId());\n }\n\n public function testTransactionRollbackOnException(): void\n {\n $activity = $this->createMock(Activity::class);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(1)\n ->willReturn($activity);\n\n $exception = new \\RuntimeException('Test exception');\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) use ($exception) {\n throw $exception;\n });\n\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: null,\n );\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('Test exception');\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testFailedMethodLogsError(): void\n {\n $exception = new \\RuntimeException('Test failure');\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: true,\n );\n\n $job->failed($exception);\n\n $this->assertTrue(true);\n }\n\n public function testLogsSuccessWhenMatchFound(): void\n {\n $participants = $this->createMock(HasMany::class);\n\n $lead = $this->createMock(Lead::class);\n $lead->method('getId')->willReturn(456);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('participants')->willReturn($participants);\n $activity->method('getLead')->willReturn($lead);\n $activity->method('getContact')->willReturn(null);\n $activity->method('getAccount')->willReturn(null);\n $activity->method('getOpportunity')->willReturn(null);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(123)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: false,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testLogsNoMatchWhenNoMatchFound(): void\n {\n $participants = $this->createMock(HasMany::class);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('participants')->willReturn($participants);\n $activity->method('getLead')->willReturn(null);\n $activity->method('getContact')->willReturn(null);\n $activity->method('getAccount')->willReturn(null);\n $activity->method('getOpportunity')->willReturn(null);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(123)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, true);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: true,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testLogsOldStateBeforeClearing(): void\n {\n $participant1 = new \\stdClass();\n $participant1->id = 10;\n $participant1->user_id = 100;\n $participant1->contact_id = 200;\n $participant1->lead_id = null;\n\n $participant2 = new \\stdClass();\n $participant2->id = 20;\n $participant2->user_id = null;\n $participant2->contact_id = null;\n $participant2->lead_id = 300;\n\n $participantsCollection = collect([$participant1, $participant2]);\n\n $participantsRelation = $this->createMock(HasMany::class);\n\n $lead = $this->createMock(Lead::class);\n $lead->method('getId')->willReturn(111);\n\n $contact = $this->createMock(Contact::class);\n $contact->method('getId')->willReturn(222);\n\n $account = $this->createMock(Account::class);\n $account->method('getId')->willReturn(333);\n\n $opportunity = $this->createMock(Opportunity::class);\n $opportunity->method('getId')->willReturn(444);\n\n $stage = $this->createMock(Stage::class);\n $stage->method('getId')->willReturn(555);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('participants')->willReturn($participantsRelation);\n $activity->method('getLead')->willReturn($lead);\n $activity->method('getContact')->willReturn($contact);\n $activity->method('getAccount')->willReturn($account);\n $activity->method('getOpportunity')->willReturn($opportunity);\n $activity->method('getStage')->willReturn($stage);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(999)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn($participantsCollection);\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 999,\n fromConfiguration: null,\n remoteSearch: false,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.16655585,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Jobs\\Crm;\n\nuse Illuminate\\Database\\Connection;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Jobs\\Crm\\MatchActivityCrmData;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActivityRepository;\nuse Jiminny\\Services\\Crm\\CrmActivityService;\nuse PHPUnit\\Framework\\MockObject\\Exception;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass MatchActivityCrmDataTest extends TestCase\n{\n private ActivityRepository|MockObject $activityRepository;\n private CrmActivityService|MockObject $crmActivityService;\n private Connection|MockObject $connection;\n\n protected function setUp(): void\n {\n parent::setUp();\n\n $this->activityRepository = $this->createMock(ActivityRepository::class);\n $this->crmActivityService = $this->createMock(CrmActivityService::class);\n $this->connection = $this->createMock(Connection::class);\n }\n\n /**\n * @throws \\Exception|Exception\n */\n public function testHandle(): void\n {\n $fromConfiguration = $this->createMock(Configuration::class);\n $fromConfiguration->method('getId')->willReturn(5);\n\n $team = $this->createMock(Team::class);\n\n $user = $this->createMock(User::class);\n $user->method('getTeam')->willReturn($team);\n\n $activityConfiguration = $this->createMock(Configuration::class);\n $activityConfiguration->method('getId')->willReturn(10);\n\n $participants = $this->createMock(HasMany::class);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getUuid')->willReturn('test');\n $activity->method('getProvider')->willReturn('provider');\n $activity->method('getUser')->willReturn($user);\n $activity->method('getCrm')->willReturn($activityConfiguration);\n $activity->method('participants')->willReturn($participants);\n $activity->expects($this->exactly(2))->method('update');\n\n $this->activityRepository->expects(($this->once()))\n ->method('findById')\n ->with(1)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: $fromConfiguration,\n );\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n /**\n * @throws \\Exception|Exception\n */\n public function testHandleNoActivity(): void\n {\n $this->activityRepository->expects(($this->once()))\n ->method('findById')\n ->with(1)\n ->willReturn(null);\n\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: null,\n );\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('[MatchActivityCrmData] Cannot find activity.');\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testSkipsCrmConfigurationSwitchWhenAlreadyMatches(): void\n {\n $fromConfiguration = $this->createMock(Configuration::class);\n $fromConfiguration->method('getId')->willReturn(5);\n\n $activityConfiguration = $this->createMock(Configuration::class);\n $activityConfiguration->method('getId')->willReturn(5);\n\n $participants = $this->createMock(HasMany::class);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrm')->willReturn($activityConfiguration);\n $activity->method('participants')->willReturn($participants);\n $activity->expects($this->once())->method('update');\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(123)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: $fromConfiguration,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testJobConfiguration(): void\n {\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: null,\n remoteSearch: false,\n );\n\n $this->assertSame(3, $job->tries);\n $this->assertSame(300, $job->timeout());\n $this->assertSame(360, $job->uniqueFor());\n $this->assertSame([30, 90, 180], $job->backoff());\n }\n\n public function testUniqueId(): void\n {\n $configuration = $this->createMock(Configuration::class);\n $configuration->method('getId')->willReturn(5);\n\n $job1 = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: false,\n );\n $this->assertSame('123:0:local', $job1->uniqueId());\n\n $job2 = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: true,\n );\n $this->assertSame('123:0:remote', $job2->uniqueId());\n\n $job3 = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: $configuration,\n remoteSearch: false,\n );\n $this->assertSame('123:5:local', $job3->uniqueId());\n }\n\n public function testTransactionRollbackOnException(): void\n {\n $activity = $this->createMock(Activity::class);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(1)\n ->willReturn($activity);\n\n $exception = new \\RuntimeException('Test exception');\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) use ($exception) {\n throw $exception;\n });\n\n $job = new MatchActivityCrmData(\n activityId: 1,\n fromConfiguration: null,\n );\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('Test exception');\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testFailedMethodLogsError(): void\n {\n $exception = new \\RuntimeException('Test failure');\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: true,\n );\n\n $job->failed($exception);\n\n $this->assertTrue(true);\n }\n\n public function testLogsSuccessWhenMatchFound(): void\n {\n $participants = $this->createMock(HasMany::class);\n\n $lead = $this->createMock(Lead::class);\n $lead->method('getId')->willReturn(456);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('participants')->willReturn($participants);\n $activity->method('getLead')->willReturn($lead);\n $activity->method('getContact')->willReturn(null);\n $activity->method('getAccount')->willReturn(null);\n $activity->method('getOpportunity')->willReturn(null);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(123)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: false,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testLogsNoMatchWhenNoMatchFound(): void\n {\n $participants = $this->createMock(HasMany::class);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('participants')->willReturn($participants);\n $activity->method('getLead')->willReturn(null);\n $activity->method('getContact')->willReturn(null);\n $activity->method('getAccount')->willReturn(null);\n $activity->method('getOpportunity')->willReturn(null);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(123)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn(collect([]));\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, true);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 123,\n fromConfiguration: null,\n remoteSearch: true,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n\n public function testLogsOldStateBeforeClearing(): void\n {\n $participant1 = new \\stdClass();\n $participant1->id = 10;\n $participant1->user_id = 100;\n $participant1->contact_id = 200;\n $participant1->lead_id = null;\n\n $participant2 = new \\stdClass();\n $participant2->id = 20;\n $participant2->user_id = null;\n $participant2->contact_id = null;\n $participant2->lead_id = 300;\n\n $participantsCollection = collect([$participant1, $participant2]);\n\n $participantsRelation = $this->createMock(HasMany::class);\n\n $lead = $this->createMock(Lead::class);\n $lead->method('getId')->willReturn(111);\n\n $contact = $this->createMock(Contact::class);\n $contact->method('getId')->willReturn(222);\n\n $account = $this->createMock(Account::class);\n $account->method('getId')->willReturn(333);\n\n $opportunity = $this->createMock(Opportunity::class);\n $opportunity->method('getId')->willReturn(444);\n\n $stage = $this->createMock(Stage::class);\n $stage->method('getId')->willReturn(555);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('participants')->willReturn($participantsRelation);\n $activity->method('getLead')->willReturn($lead);\n $activity->method('getContact')->willReturn($contact);\n $activity->method('getAccount')->willReturn($account);\n $activity->method('getOpportunity')->willReturn($opportunity);\n $activity->method('getStage')->willReturn($stage);\n\n $this->activityRepository->expects($this->once())\n ->method('findById')\n ->with(999)\n ->willReturn($activity);\n\n $this->activityRepository->expects($this->once())\n ->method('getActivityParticipants')\n ->with($activity)\n ->willReturn($participantsCollection);\n\n $this->crmActivityService->expects($this->once())\n ->method('updateCrmData')\n ->with($activity, false);\n\n $this->connection->expects($this->once())\n ->method('transaction')\n ->willReturnCallback(function ($callback) {\n return $callback();\n });\n\n $job = new MatchActivityCrmData(\n activityId: 999,\n fromConfiguration: null,\n remoteSearch: false,\n );\n\n $job->handle(\n activityRepository: $this->activityRepository,\n crmActivityService: $this->crmActivityService,\n connection: $this->connection,\n );\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5635657910962254791
|
-5188939258130706600
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmDataTest.php, class checked
Services/Crm/Hubspot/Pagination 1 file, folder checked
HubspotPaginationServiceTest.php, class checked
.env.local not checked
app/Console/Commands 1 file, folder not checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmDataTest.php, class checked
Services/Crm/Hubspot/Pagination 1 file, folder checked
HubspotPaginationServiceTest.php, class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmDataTest.php, class checked
MatchActivityCrmDataTest.php, class checked
Services/Crm/Hubspot/Pagination 1 file, folder checked
HubspotPaginationServiceTest.php, class checked
HubspotPaginationServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
2 modified
JY-20725 add HS rate limit handling on activities rematching
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
44d5678c
<?php
declare(strict_types=1);
namespace Tests\Unit\Jobs\Crm;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Jobs\Crm\MatchActivityCrmData;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\ActivityRepository;
use Jiminny\Services\Crm\CrmActivityService;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class MatchActivityCrmDataTest extends TestCase
{
private ActivityRepository|MockObject $activityRepository;
private CrmActivityService|MockObject $crmActivityService;
private Connection|MockObject $connection;
protected function setUp(): void
{
parent::setUp();
$this->activityRepository = $this->createMock(ActivityRepository::class);
$this->crmActivityService = $this->createMock(CrmActivityService::class);
$this->connection = $this->createMock(Connection::class);
}
/**
* @throws \Exception|Exception
*/
public function testHandle(): void
{
$fromConfiguration = $this->createMock(Configuration::class);
$fromConfiguration->method('getId')->willReturn(5);
$team = $this->createMock(Team::class);
$user = $this->createMock(User::class);
$user->method('getTeam')->willReturn($team);
$activityConfiguration = $this->createMock(Configuration::class);
$activityConfiguration->method('getId')->willReturn(10);
$participants = $this->createMock(HasMany::class);
$activity = $this->createMock(Activity::class);
$activity->method('getUuid')->willReturn('test');
$activity->method('getProvider')->willReturn('provider');
$activity->method('getUser')->willReturn($user);
$activity->method('getCrm')->willReturn($activityConfiguration);
$activity->method('participants')->willReturn($participants);
$activity->expects($this->exactly(2))->method('update');
$this->activityRepository->expects(($this->once()))
->method('findById')
->with(1)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: $fromConfiguration,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
/**
* @throws \Exception|Exception
*/
public function testHandleNoActivity(): void
{
$this->activityRepository->expects(($this->once()))
->method('findById')
->with(1)
->willReturn(null);
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: null,
);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('[MatchActivityCrmData] Cannot find activity.');
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testSkipsCrmConfigurationSwitchWhenAlreadyMatches(): void
{
$fromConfiguration = $this->createMock(Configuration::class);
$fromConfiguration->method('getId')->willReturn(5);
$activityConfiguration = $this->createMock(Configuration::class);
$activityConfiguration->method('getId')->willReturn(5);
$participants = $this->createMock(HasMany::class);
$activity = $this->createMock(Activity::class);
$activity->method('getCrm')->willReturn($activityConfiguration);
$activity->method('participants')->willReturn($participants);
$activity->expects($this->once())->method('update');
$this->activityRepository->expects($this->once())
->method('findById')
->with(123)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: $fromConfiguration,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testJobConfiguration(): void
{
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: null,
remoteSearch: false,
);
$this->assertSame(3, $job->tries);
$this->assertSame(300, $job->timeout());
$this->assertSame(360, $job->uniqueFor());
$this->assertSame([30, 90, 180], $job->backoff());
}
public function testUniqueId(): void
{
$configuration = $this->createMock(Configuration::class);
$configuration->method('getId')->willReturn(5);
$job1 = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: false,
);
$this->assertSame('123:0:local', $job1->uniqueId());
$job2 = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: true,
);
$this->assertSame('123:0:remote', $job2->uniqueId());
$job3 = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: $configuration,
remoteSearch: false,
);
$this->assertSame('123:5:local', $job3->uniqueId());
}
public function testTransactionRollbackOnException(): void
{
$activity = $this->createMock(Activity::class);
$this->activityRepository->expects($this->once())
->method('findById')
->with(1)
->willReturn($activity);
$exception = new \RuntimeException('Test exception');
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) use ($exception) {
throw $exception;
});
$job = new MatchActivityCrmData(
activityId: 1,
fromConfiguration: null,
);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Test exception');
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testFailedMethodLogsError(): void
{
$exception = new \RuntimeException('Test failure');
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: true,
);
$job->failed($exception);
$this->assertTrue(true);
}
public function testLogsSuccessWhenMatchFound(): void
{
$participants = $this->createMock(HasMany::class);
$lead = $this->createMock(Lead::class);
$lead->method('getId')->willReturn(456);
$activity = $this->createMock(Activity::class);
$activity->method('participants')->willReturn($participants);
$activity->method('getLead')->willReturn($lead);
$activity->method('getContact')->willReturn(null);
$activity->method('getAccount')->willReturn(null);
$activity->method('getOpportunity')->willReturn(null);
$this->activityRepository->expects($this->once())
->method('findById')
->with(123)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: false,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testLogsNoMatchWhenNoMatchFound(): void
{
$participants = $this->createMock(HasMany::class);
$activity = $this->createMock(Activity::class);
$activity->method('participants')->willReturn($participants);
$activity->method('getLead')->willReturn(null);
$activity->method('getContact')->willReturn(null);
$activity->method('getAccount')->willReturn(null);
$activity->method('getOpportunity')->willReturn(null);
$this->activityRepository->expects($this->once())
->method('findById')
->with(123)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn(collect([]));
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, true);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 123,
fromConfiguration: null,
remoteSearch: true,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
public function testLogsOldStateBeforeClearing(): void
{
$participant1 = new \stdClass();
$participant1->id = 10;
$participant1->user_id = 100;
$participant1->contact_id = 200;
$participant1->lead_id = null;
$participant2 = new \stdClass();
$participant2->id = 20;
$participant2->user_id = null;
$participant2->contact_id = null;
$participant2->lead_id = 300;
$participantsCollection = collect([$participant1, $participant2]);
$participantsRelation = $this->createMock(HasMany::class);
$lead = $this->createMock(Lead::class);
$lead->method('getId')->willReturn(111);
$contact = $this->createMock(Contact::class);
$contact->method('getId')->willReturn(222);
$account = $this->createMock(Account::class);
$account->method('getId')->willReturn(333);
$opportunity = $this->createMock(Opportunity::class);
$opportunity->method('getId')->willReturn(444);
$stage = $this->createMock(Stage::class);
$stage->method('getId')->willReturn(555);
$activity = $this->createMock(Activity::class);
$activity->method('participants')->willReturn($participantsRelation);
$activity->method('getLead')->willReturn($lead);
$activity->method('getContact')->willReturn($contact);
$activity->method('getAccount')->willReturn($account);
$activity->method('getOpportunity')->willReturn($opportunity);
$activity->method('getStage')->willReturn($stage);
$this->activityRepository->expects($this->once())
->method('findById')
->with(999)
->willReturn($activity);
$this->activityRepository->expects($this->once())
->method('getActivityParticipants')
->with($activity)
->willReturn($participantsCollection);
$this->crmActivityService->expects($this->once())
->method('updateCrmData')
->with($activity, false);
$this->connection->expects($this->once())
->method('transaction')
->willReturnCallback(function ($callback) {
return $callback();
});
$job = new MatchActivityCrmData(
activityId: 999,
fromConfiguration: null,
remoteSearch: false,
);
$job->handle(
activityRepository: $this->activityRepository,
crmActivityService: $this->crmActivityService,
connection: $this->connection,
);
}
}...
|
19629
|
NULL
|
NULL
|
NULL
|
|
20618
|
897
|
1
|
2026-05-11T16:00:48.766004+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515248766_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-9042236301121362992
|
-2257098041361954999
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference...
|
20612
|
NULL
|
NULL
|
NULL
|
|
20617
|
896
|
1
|
2026-05-11T16:00:48.773811+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515248773_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1061794090295347552
|
-2257102447998335423
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference...
|
20609
|
NULL
|
NULL
|
NULL
|
|
20619
|
896
|
2
|
2026-05-11T16:00:49.500857+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515249500_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6499465471169678443
|
-1896744100474581237
|
click
|
hybrid
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpDOCKERAPP (-zsh)-zsh+ +- ₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh> 0 hhl*5screenpipe"100% <78• Mon 11 May 19:00:49T₴1O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ 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 Ruminski and contributors.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".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20620
|
897
|
2
|
2026-05-11T16:00:49.500881+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515249500_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"4 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"}]...
|
749701607339914434
|
-2257032345542127861
|
click
|
hybrid
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
PhostormVIewINavigarecodeLaravelKeractorrTavsco.s°9 JY-20725-handle-HS-search-rateProiect>@ Mailbo›C) UserAutomatedReportscontroller.ongv D Middlewareo handleruospotkal>@ Streaming© MatchActivityCrmData.php>D Team_ lelepnony> DUser© ImportRecallAlRecordi<?ohpdeclarelstrict tyoess.o:c) SasVisibilitycontrolTe:v W Listenersnamespace Tests Unit Jobs MiddLeware:> @ Activities>D Audio• AutomatedReports› use ...Autoscore17 Crm#[CoversClass(HandleHubspotRateLimit::class)]18 VAclass HandleluhsnotRatel imitTest extends TecttaseM DealRisks17 ElasticSearchlGrouosprivate HandleHubspotRateLimit $middleware;Import• M MailbosCancola yLocal ChangesLog X• Chanaes & files= env.local aor.8 02d5214b app/ExC) Client.oho aon/Services/Crm/Hubsootc ClientTect nhn tectc/Unit/Services/Crm/Huhsnotuse Ihrowable© HandleHubspotRateLimitTest.php tests/Unit/Jobs/Middleware© JiminnyDebugCommand.php app/Console/Commandsclass ratelimitphp logging.php config© MatchActivityCrmData.php app/Jobs/Crmpublic fun© RateLimitException.php app/Exceptionsstrind• Iinvorcinnod Siloc @ filacE.env.nikilocal appE.env.other app© CanAccessAiReportsTest.php tests/Unit/Policies?Throwanarent© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Rli tavicon.ico public=ids.txt apdnublic function aetRetrvAftero. intTa raw sal querv.sal app© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspotreturn max(Sthis->retryAfter, 1)M.WEBTOOK FILTERING IMPLEMENTATION.mo a00WindowmelpRematchActivityOnCrmObjectDetach.php© HubspotPaginationService.phpC) TrackAutomated Revori Generaledeventonp© Hubspot/Service.phpOhubspot/service.pnp© SyncCrmEntitiesTrait.phpC) CachedCrmServiceDecorator.ong©) HandleHubspotRateLimitTest.php x© CheckAndRetryRemoteMatch.phpCrmactivityservice.png olCommit ChanaesChangelist: Changes• M Exceptions 1 fileAuthor:RateLimitException.phpvaJobs/Crm 1 fileAmend commitSian-oft commitV © MatchActivityCrmData.php5 modifiedCommit checksCommit MessageJY-20725 code revlew suggestionsUpdate copvriahiPoformat codeRearrange codeOpumize imporisv Diftsideoy-slde viewer@ 02d5214b app/Exceptions/RateLimitException.phyuse InrowablerDo notignoreHighlight woras4 omerencesCurrent versioruse Throwahl p:class RateLimitException extends RuntimeExceptionclass RateLimitException extends RuntimeExceptionpublic function __construct(string Smessage = "'private readonly int SretryAfter = 1?Throwable Sprevious = nullO' 1111 Vprivate readonly int SretryAfter;public function __construct(string $message = "'parent:: construct(Smessage. 0, Sprevious):Int srecryAtter = 1Thnownbla Cnnovioue - nulll17Cance100% Lz• Mon 11 May 19:00:49A SF [jiminny@localhost]4 HS_local [jiminny@localhost]A console [PROD]« console [EU]A console [STAGING]"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEs0kXi ~07-May-26 14:51:15 GMT: domain=.hubapi.com:Http0nly: Secure: SameSite=None"].w19A"keporc-10"."ppoints":li-":1"https:(VAVa.nel.cloudfLare.com/V/report\V/v4?s=NYALsVTP0fYm52qrsDJxYE4sd2RWRq15p5wHSmd=g<LzoYdx1x2B1XVpHmsKnS0x2BKVA5mFLJ2m/YRECD65M62BW2LYT206FM4/2l vbupl": l"cf-nel!",kage ":604800"n:["€ccess traction":0.01.bort to ": "cf-nel".k ade":604800-"Ven":"cloudflare">elation_id":"95236535-ec98-4541-b92a-adfa73b69eab",he id"."c7ab8365-903f-46d4-9403-0e5b551e3545"}4 differencescurrent versionclass RateLimitException extends RuntimeExceptionprivate readonly int SretrvAfter:public function __construct(strina Smessage = "'int SretryAfter = 1,¿Throwable Sorevious = null.narent.• construet(Smessage. A. Sorevious):Sthis->retrvAften = max(1. SretrvAfter)•public function getRetryAfterQ: intreturn $this->retryAfter:W Windsurf Teamsfo 4 spaces 0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20609
|
894
|
11
|
2026-05-11T15:58:51.515401+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515131515_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20610
|
895
|
17
|
2026-05-11T15:58:51.633550+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515131633_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"4 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.023603724,"height":0.013567438},"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.14827128,"height":0.42138866},"on_screen":true,"lines":[{"char_start":0,"char_count":6,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":7,"char_count":25,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":33,"char_count":30,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":64,"char_count":15,"bounds":{"left":0.2613032,"top":0.16280925,"width":0.036236703,"height":0.014365523}},{"char_start":80,"char_count":50,"bounds":{"left":0.2613032,"top":0.19792499,"width":0.12699468,"height":0.014365523}},{"char_start":130,"char_count":2,"bounds":{"left":0.2613032,"top":0.21548285,"width":0.0026595744,"height":0.014365523}},{"char_start":132,"char_count":33,"bounds":{"left":0.2613032,"top":0.2330407,"width":0.08277926,"height":0.014365523}},{"char_start":165,"char_count":30,"bounds":{"left":0.2613032,"top":0.25059855,"width":0.07513298,"height":0.014365523}},{"char_start":195,"char_count":46,"bounds":{"left":0.2613032,"top":0.26815644,"width":0.11635638,"height":0.014365523}},{"char_start":241,"char_count":37,"bounds":{"left":0.2613032,"top":0.2857143,"width":0.0930851,"height":0.014365523}},{"char_start":278,"char_count":8,"bounds":{"left":0.2613032,"top":0.30327216,"width":0.017952127,"height":0.014365523}},{"char_start":286,"char_count":53,"bounds":{"left":0.2613032,"top":0.32083002,"width":0.13464096,"height":0.014365523}},{"char_start":339,"char_count":6,"bounds":{"left":0.2613032,"top":0.33838788,"width":0.012965426,"height":0.014365523}},{"char_start":346,"char_count":41,"bounds":{"left":0.2613032,"top":0.3735036,"width":0.10372341,"height":0.014365523}},{"char_start":387,"char_count":6,"bounds":{"left":0.2613032,"top":0.39106146,"width":0.012965426,"height":0.014365523}},{"char_start":393,"char_count":42,"bounds":{"left":0.2613032,"top":0.4086193,"width":0.10605053,"height":0.014365523}},{"char_start":435,"char_count":6,"bounds":{"left":0.2613032,"top":0.42617717,"width":0.012965426,"height":0.014365523}},{"char_start":441,"char_count":1,"bounds":{"left":0.2613032,"top":0.44373503,"width":0.0026595744,"height":0.014365523}}],"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4348404,"top":0.43415803,"width":0.14361702,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"bounds":{"left":0.43949467,"top":0.42697525,"width":0.14228724,"height":0.47406226},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
20608
|
NULL
|
NULL
|
NULL
|
|
20611
|
894
|
12
|
2026-05-11T15:59:21.848496+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515161848_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
20609
|
NULL
|
NULL
|
NULL
|
|
20612
|
895
|
18
|
2026-05-11T15:59:22.097210+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515162097_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"4 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.023603724,"height":0.013567438},"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.14827128,"height":0.42138866},"on_screen":true,"lines":[{"char_start":0,"char_count":6,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":7,"char_count":25,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":33,"char_count":30,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":64,"char_count":15,"bounds":{"left":0.2613032,"top":0.16280925,"width":0.036236703,"height":0.014365523}},{"char_start":80,"char_count":50,"bounds":{"left":0.2613032,"top":0.19792499,"width":0.12699468,"height":0.014365523}},{"char_start":130,"char_count":2,"bounds":{"left":0.2613032,"top":0.21548285,"width":0.0026595744,"height":0.014365523}},{"char_start":132,"char_count":33,"bounds":{"left":0.2613032,"top":0.2330407,"width":0.08277926,"height":0.014365523}},{"char_start":165,"char_count":30,"bounds":{"left":0.2613032,"top":0.25059855,"width":0.07513298,"height":0.014365523}},{"char_start":195,"char_count":46,"bounds":{"left":0.2613032,"top":0.26815644,"width":0.11635638,"height":0.014365523}},{"char_start":241,"char_count":37,"bounds":{"left":0.2613032,"top":0.2857143,"width":0.0930851,"height":0.014365523}},{"char_start":278,"char_count":8,"bounds":{"left":0.2613032,"top":0.30327216,"width":0.017952127,"height":0.014365523}},{"char_start":286,"char_count":53,"bounds":{"left":0.2613032,"top":0.32083002,"width":0.13464096,"height":0.014365523}},{"char_start":339,"char_count":6,"bounds":{"left":0.2613032,"top":0.33838788,"width":0.012965426,"height":0.014365523}},{"char_start":346,"char_count":41,"bounds":{"left":0.2613032,"top":0.3735036,"width":0.10372341,"height":0.014365523}},{"char_start":387,"char_count":6,"bounds":{"left":0.2613032,"top":0.39106146,"width":0.012965426,"height":0.014365523}},{"char_start":393,"char_count":42,"bounds":{"left":0.2613032,"top":0.4086193,"width":0.10605053,"height":0.014365523}},{"char_start":435,"char_count":6,"bounds":{"left":0.2613032,"top":0.42617717,"width":0.012965426,"height":0.014365523}},{"char_start":441,"char_count":1,"bounds":{"left":0.2613032,"top":0.44373503,"width":0.0026595744,"height":0.014365523}}],"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4348404,"top":0.43415803,"width":0.14361702,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"bounds":{"left":0.43949467,"top":0.42697525,"width":0.14228724,"height":0.47406226},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20613
|
NULL
|
0
|
2026-05-11T15:59:52.164811+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515192164_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
20609
|
NULL
|
NULL
|
NULL
|
|
20614
|
NULL
|
0
|
2026-05-11T15:59:52.420141+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515192420_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"4 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.023603724,"height":0.013567438},"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.14827128,"height":0.42138866},"on_screen":true,"lines":[{"char_start":0,"char_count":6,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":7,"char_count":25,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":33,"char_count":30,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":64,"char_count":15,"bounds":{"left":0.2613032,"top":0.16280925,"width":0.036236703,"height":0.014365523}},{"char_start":80,"char_count":50,"bounds":{"left":0.2613032,"top":0.19792499,"width":0.12699468,"height":0.014365523}},{"char_start":130,"char_count":2,"bounds":{"left":0.2613032,"top":0.21548285,"width":0.0026595744,"height":0.014365523}},{"char_start":132,"char_count":33,"bounds":{"left":0.2613032,"top":0.2330407,"width":0.08277926,"height":0.014365523}},{"char_start":165,"char_count":30,"bounds":{"left":0.2613032,"top":0.25059855,"width":0.07513298,"height":0.014365523}},{"char_start":195,"char_count":46,"bounds":{"left":0.2613032,"top":0.26815644,"width":0.11635638,"height":0.014365523}},{"char_start":241,"char_count":37,"bounds":{"left":0.2613032,"top":0.2857143,"width":0.0930851,"height":0.014365523}},{"char_start":278,"char_count":8,"bounds":{"left":0.2613032,"top":0.30327216,"width":0.017952127,"height":0.014365523}},{"char_start":286,"char_count":53,"bounds":{"left":0.2613032,"top":0.32083002,"width":0.13464096,"height":0.014365523}},{"char_start":339,"char_count":6,"bounds":{"left":0.2613032,"top":0.33838788,"width":0.012965426,"height":0.014365523}},{"char_start":346,"char_count":41,"bounds":{"left":0.2613032,"top":0.3735036,"width":0.10372341,"height":0.014365523}},{"char_start":387,"char_count":6,"bounds":{"left":0.2613032,"top":0.39106146,"width":0.012965426,"height":0.014365523}},{"char_start":393,"char_count":42,"bounds":{"left":0.2613032,"top":0.4086193,"width":0.10605053,"height":0.014365523}},{"char_start":435,"char_count":6,"bounds":{"left":0.2613032,"top":0.42617717,"width":0.012965426,"height":0.014365523}},{"char_start":441,"char_count":1,"bounds":{"left":0.2613032,"top":0.44373503,"width":0.0026595744,"height":0.014365523}}],"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4348404,"top":0.43415803,"width":0.14361702,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"bounds":{"left":0.43949467,"top":0.42697525,"width":0.14228724,"height":0.47406226},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
20612
|
NULL
|
NULL
|
NULL
|
|
20615
|
896
|
0
|
2026-05-11T16:00:22.342469+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515222342_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
20609
|
NULL
|
NULL
|
NULL
|
|
20616
|
897
|
0
|
2026-05-11T16:00:22.717316+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515222717_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code review suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code review suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.66081405,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"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":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"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":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"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":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"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":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"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":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"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 Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"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":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"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":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"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":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"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":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"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":"4 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"02d5214b","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.023603724,"height":0.013567438},"on_screen":true,"value":"02d5214b","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.14827128,"height":0.42138866},"on_screen":true,"lines":[{"char_start":0,"char_count":6,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":7,"char_count":25,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":33,"char_count":30,"bounds":{"left":0.2613032,"top":0.1452514,"width":0.0026595744,"height":0.014365523}},{"char_start":64,"char_count":15,"bounds":{"left":0.2613032,"top":0.16280925,"width":0.036236703,"height":0.014365523}},{"char_start":80,"char_count":50,"bounds":{"left":0.2613032,"top":0.19792499,"width":0.12699468,"height":0.014365523}},{"char_start":130,"char_count":2,"bounds":{"left":0.2613032,"top":0.21548285,"width":0.0026595744,"height":0.014365523}},{"char_start":132,"char_count":33,"bounds":{"left":0.2613032,"top":0.2330407,"width":0.08277926,"height":0.014365523}},{"char_start":165,"char_count":30,"bounds":{"left":0.2613032,"top":0.25059855,"width":0.07513298,"height":0.014365523}},{"char_start":195,"char_count":46,"bounds":{"left":0.2613032,"top":0.26815644,"width":0.11635638,"height":0.014365523}},{"char_start":241,"char_count":37,"bounds":{"left":0.2613032,"top":0.2857143,"width":0.0930851,"height":0.014365523}},{"char_start":278,"char_count":8,"bounds":{"left":0.2613032,"top":0.30327216,"width":0.017952127,"height":0.014365523}},{"char_start":286,"char_count":53,"bounds":{"left":0.2613032,"top":0.32083002,"width":0.13464096,"height":0.014365523}},{"char_start":339,"char_count":6,"bounds":{"left":0.2613032,"top":0.33838788,"width":0.012965426,"height":0.014365523}},{"char_start":346,"char_count":41,"bounds":{"left":0.2613032,"top":0.3735036,"width":0.10372341,"height":0.014365523}},{"char_start":387,"char_count":6,"bounds":{"left":0.2613032,"top":0.39106146,"width":0.012965426,"height":0.014365523}},{"char_start":393,"char_count":42,"bounds":{"left":0.2613032,"top":0.4086193,"width":0.10605053,"height":0.014365523}},{"char_start":435,"char_count":6,"bounds":{"left":0.2613032,"top":0.42617717,"width":0.012965426,"height":0.014365523}},{"char_start":441,"char_count":1,"bounds":{"left":0.2613032,"top":0.44373503,"width":0.0026595744,"height":0.014365523}}],"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4348404,"top":0.43415803,"width":0.14361702,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","depth":2,"bounds":{"left":0.43949467,"top":0.42697525,"width":0.14228724,"height":0.47406226},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends RuntimeException\n{\n private readonly int $retryAfter;\n\n public function __construct(\n string $message = '',\n int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n $this->retryAfter = max(1, $retryAfter);\n }\n\n public function getRetryAfter(): int\n {\n return $this->retryAfter;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-654605678890122259
|
-2543036966578944697
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code review suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
4 differences
02d5214b
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends RuntimeException
{
private readonly int $retryAfter;
public function __construct(
string $message = '',
int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
$this->retryAfter = max(1, $retryAfter);
}
public function getRetryAfter(): int
{
return $this->retryAfter;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
20612
|
NULL
|
NULL
|
NULL
|
|
20607
|
894
|
10
|
2026-05-11T15:58:21.188073+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515101188_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code revuiew suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:...
|
[{"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":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code revuiew suggestions","depth":2,"on_screen":true,"value":"JY-20725 code revuiew suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9043461944617271370
|
-1750447490055534783
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code revuiew suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:...
|
20605
|
NULL
|
NULL
|
NULL
|
|
20608
|
895
|
16
|
2026-05-11T15:58:21.175576+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515101175_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
5 modified
Analyzing…
JY-20725 code revuiew suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:...
|
[{"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":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 code revuiew suggestions","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 code revuiew suggestions","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Find unused view files","depth":2,"bounds":{"left":0.4587766,"top":0.481245,"width":0.054521278,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.5235435,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.54668796,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5666401,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.5698324,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.6089386,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.6360734,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.62968874,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9043461944617271370
|
-1750447490055534783
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 modified
Analyzing…
JY-20725 code revuiew suggestions
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Find unused view files
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20606
|
895
|
15
|
2026-05-11T15:58:09.363966+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515089363_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class 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":"8 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.038231384,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.29130086,"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.30885875,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.39664805,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.038231384,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"}]...
|
-7526893880074404688
|
-420479140016020156
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked...
|
20604
|
NULL
|
NULL
|
NULL
|
|
20605
|
894
|
9
|
2026-05-11T15:58:09.383164+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515089383_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.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":"8 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":5,"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":"Exceptions 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, 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":"Jobs/Middleware 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.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 4 files, folder partially checked","depth":4,"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":"Exceptions 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":5,"on_screen":true,"role_description":"text"}]...
|
4052118162170349272
|
-420479140016020156
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20604
|
895
|
14
|
2026-05-11T15:58:08.343322+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515088343_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
5 modified
Analyzing…
JY-20725 add more tests
Commit Message
Commit Message History
Git...
|
[{"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":"8 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.038231384,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.29130086,"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.30885875,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.39664805,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.038231384,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.29130086,"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.30885875,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.39664805,"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.4142059,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 add more tests","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 add more tests","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"}]...
|
-5253823119243673684
|
8793885439885845836
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
5 modified
Analyzing…
JY-20725 add more tests
Commit Message
Commit Message History
Git...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20603
|
894
|
8
|
2026-05-11T15:58:08.346505+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515088346_m1.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
5 modified
Analyzing…
JY-20725 add more tests
Commit Message
Commit Message History
Git
Author:
Author:...
|
[{"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":"8 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":5,"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":"Exceptions 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, 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":"Jobs/Middleware 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.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 4 files, folder partially checked","depth":4,"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":"Exceptions 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit 2 files, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unversioned Files 9 files not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 add more tests","depth":2,"on_screen":true,"value":"JY-20725 add more tests","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
753241680846487865
|
8829984605648987468
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
5 modified
Analyzing…
JY-20725 add more tests
Commit Message
Commit Message History
Git
Author:
Author:...
|
20600
|
NULL
|
NULL
|
NULL
|
|
20602
|
895
|
13
|
2026-05-11T15:58:05.578330+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778515085578_m2.jpg...
|
PhpStorm
|
Commit Changes
|
True
|
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
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
5 modified
Analyzing…
JY-20725 add more tests
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright...
|
[{"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":"8 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.038231384,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.29130086,"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.30885875,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.39664805,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 4 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.038231384,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.049867023,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitException.php, exception class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Crm 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.22106944,"width":0.047539894,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchActivityCrmData.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.2386273,"width":0.071476065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.273743,"width":0.036901597,"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.29130086,"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.30885875,"width":0.050531916,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Middleware 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.3264166,"width":0.062832445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HandleHubspotRateLimitTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.08510638,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Crm/Hubspot 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.36153233,"width":0.07413564,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ClientTest.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.3790902,"width":0.046210106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.39664805,"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.4142059,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20725 add more tests","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20725 add more tests","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"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":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7831640914639698295
|
8802892914018493772
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 files, folder partially checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
.env.local not checked
app 4 files, folder partially checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Console/Commands 1 file, folder not checked
Exceptions 1 file, folder checked
RateLimitException.php, exception class checked
RateLimitException.php, exception class checked
Jobs/Crm 1 file, folder checked
MatchActivityCrmData.php, class checked
MatchActivityCrmData.php, class checked
Services/Crm/Hubspot 1 file, folder checked
Client.php, class checked
Client.php, class checked
config 1 file, folder not checked
tests/Unit 2 files, folder checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
Jobs/Middleware 1 file, folder checked
HandleHubspotRateLimitTest.php, class checked
HandleHubspotRateLimitTest.php, class checked
Services/Crm/Hubspot 1 file, folder checked
ClientTest.php, class checked
ClientTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
5 modified
Analyzing…
JY-20725 add more tests
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright...
|
20599
|
NULL
|
NULL
|
NULL
|
|
5362
|
193
|
2
|
2026-05-07T15:18:51.922382+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167131922_m1.jpg...
|
CleanShot X
|
CleanShot
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Convert
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Dimensions:","depth":2,"on_screen":true,"automation_id":"_NS:29","role_description":"text"},{"role":"AXStaticText","text":"Width:","depth":2,"on_screen":true,"automation_id":"_NS:67","role_description":"text"},{"role":"AXStaticText","text":"Height:","depth":2,"on_screen":true,"automation_id":"_NS:71","role_description":"text"},{"role":"AXTextField","text":"1812","depth":2,"on_screen":true,"automation_id":"_NS:75","value":"1812","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXTextField","text":"1080","depth":2,"on_screen":true,"automation_id":"_NS:83","value":"1080","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Video quality:","depth":2,"on_screen":true,"automation_id":"_NS:86","role_description":"text"},{"role":"AXStaticText","text":"Ultra","depth":2,"on_screen":true,"automation_id":"_NS:94","role_description":"text"},{"role":"AXButton","text":"Convert","depth":1,"on_screen":true,"automation_id":"_NS:109","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"automation_id":"_NS:118","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Estimated file size: ~1,66 GB","depth":1,"on_screen":true,"automation_id":"_NS:123","role_description":"text"},{"role":"AXCheckBox","text":"play/pause","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":true},{"role":"AXStaticText","text":"Audio:","depth":2,"on_screen":true,"automation_id":"_NS:136","role_description":"text"},{"role":"AXRadioButton","text":"Convert to mono","depth":2,"on_screen":true,"automation_id":"_NS:140","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Don't change","depth":2,"on_screen":true,"automation_id":"_NS:147","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Mute","depth":2,"on_screen":true,"automation_id":"_NS:151","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"0%","depth":2,"on_screen":true,"automation_id":"_NS:155","role_description":"text"},{"role":"AXRadioButton","text":"Change volume","depth":2,"on_screen":true,"automation_id":"_NS:164","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"100%","depth":2,"on_screen":true,"automation_id":"_NS:168","role_description":"text"},{"role":"AXStaticText","text":"200%","depth":2,"on_screen":true,"automation_id":"_NS:172","role_description":"text"}]...
|
-6104071325232410165
|
-7252843957429087068
|
visual_change
|
accessibility
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Convert
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
5365
|
194
|
3
|
2026-05-07T15:19:02.862367+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167142862_m2.jpg...
|
CleanShot X
|
CleanShot
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
01:21,33...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Dimensions:","depth":2,"bounds":{"left":0.3879654,"top":0.54668796,"width":0.02825798,"height":0.012769354},"on_screen":true,"automation_id":"_NS:29","role_description":"text"},{"role":"AXStaticText","text":"Width:","depth":2,"bounds":{"left":0.39162233,"top":0.57701516,"width":0.024601065,"height":0.012769354},"on_screen":true,"automation_id":"_NS:67","role_description":"text"},{"role":"AXStaticText","text":"Height:","depth":2,"bounds":{"left":0.38962767,"top":0.6017558,"width":0.026595745,"height":0.012769354},"on_screen":true,"automation_id":"_NS:71","role_description":"text"},{"role":"AXTextField","text":"1812","depth":2,"bounds":{"left":0.41821808,"top":0.575419,"width":0.024933511,"height":0.016759777},"on_screen":true,"automation_id":"_NS:75","value":"1812","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXTextField","text":"1080","depth":2,"bounds":{"left":0.41821808,"top":0.6017558,"width":0.024933511,"height":0.016759777},"on_screen":true,"automation_id":"_NS:83","value":"1080","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Video quality:","depth":2,"bounds":{"left":0.38730052,"top":0.5139665,"width":0.028922873,"height":0.012769354},"on_screen":true,"automation_id":"_NS:86","role_description":"text"},{"role":"AXStaticText","text":"Ultra","depth":2,"bounds":{"left":0.47839096,"top":0.51476455,"width":0.018949468,"height":0.012769354},"on_screen":true,"automation_id":"_NS:94","role_description":"text"},{"role":"AXButton","text":"Trim & Convert","depth":1,"bounds":{"left":0.5787899,"top":0.66480446,"width":0.04155585,"height":0.025538707},"on_screen":true,"automation_id":"_NS:109","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Trim Only","depth":1,"bounds":{"left":0.54886967,"top":0.66480446,"width":0.030585106,"height":0.025538707},"on_screen":true,"automation_id":"_NS:114","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.37965426,"top":0.66480446,"width":0.025265958,"height":0.025538707},"on_screen":true,"automation_id":"_NS:118","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Estimated file size: ~1,66 GB","depth":1,"bounds":{"left":0.40990692,"top":0.669593,"width":0.05867686,"height":0.012769354},"on_screen":true,"automation_id":"_NS:123","role_description":"text"},{"role":"AXCheckBox","text":"play/pause","depth":1,"bounds":{"left":0.38464096,"top":0.43216282,"width":0.020611702,"height":0.050678372},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Audio:","depth":2,"bounds":{"left":0.50482047,"top":0.5123703,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:136","role_description":"text"},{"role":"AXRadioButton","text":"Convert to mono","depth":2,"bounds":{"left":0.53075135,"top":0.5339186,"width":0.04255319,"height":0.014365523},"on_screen":true,"automation_id":"_NS:140","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Don't change","depth":2,"bounds":{"left":0.53075135,"top":0.5123703,"width":0.035904255,"height":0.014365523},"on_screen":true,"automation_id":"_NS:147","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Mute","depth":2,"bounds":{"left":0.53075135,"top":0.5554669,"width":0.018949468,"height":0.014365523},"on_screen":true,"automation_id":"_NS:151","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"0%","depth":2,"bounds":{"left":0.53706783,"top":0.6113328,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:155","role_description":"text"},{"role":"AXRadioButton","text":"Change volume","depth":2,"bounds":{"left":0.53075135,"top":0.57701516,"width":0.039893616,"height":0.014365523},"on_screen":true,"automation_id":"_NS:164","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"100%","depth":2,"bounds":{"left":0.56200135,"top":0.612929,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:168","role_description":"text"},{"role":"AXStaticText","text":"200%","depth":2,"bounds":{"left":0.5859375,"top":0.6113328,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:172","role_description":"text"},{"role":"AXStaticText","text":"01:21,33","depth":2,"bounds":{"left":0.40824467,"top":0.4102155,"width":0.020279255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-1204665730415856134
|
-4947017300606872948
|
click
|
accessibility
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
01:21,33...
|
5364
|
NULL
|
NULL
|
NULL
|
|
5366
|
193
|
3
|
2026-05-07T15:19:02.962096+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167142962_m1.jpg...
|
CleanShot X
|
CleanShot
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
03:36,22...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Dimensions:","depth":2,"on_screen":true,"automation_id":"_NS:29","role_description":"text"},{"role":"AXStaticText","text":"Width:","depth":2,"on_screen":true,"automation_id":"_NS:67","role_description":"text"},{"role":"AXStaticText","text":"Height:","depth":2,"on_screen":true,"automation_id":"_NS:71","role_description":"text"},{"role":"AXTextField","text":"1812","depth":2,"on_screen":true,"automation_id":"_NS:75","value":"1812","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXTextField","text":"1080","depth":2,"on_screen":true,"automation_id":"_NS:83","value":"1080","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Video quality:","depth":2,"on_screen":true,"automation_id":"_NS:86","role_description":"text"},{"role":"AXStaticText","text":"Ultra","depth":2,"on_screen":true,"automation_id":"_NS:94","role_description":"text"},{"role":"AXButton","text":"Trim & Convert","depth":1,"on_screen":true,"automation_id":"_NS:109","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Trim Only","depth":1,"on_screen":true,"automation_id":"_NS:114","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"automation_id":"_NS:118","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Estimated file size: ~1,66 GB","depth":1,"on_screen":true,"automation_id":"_NS:123","role_description":"text"},{"role":"AXCheckBox","text":"play/pause","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Audio:","depth":2,"on_screen":true,"automation_id":"_NS:136","role_description":"text"},{"role":"AXRadioButton","text":"Convert to mono","depth":2,"on_screen":true,"automation_id":"_NS:140","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Don't change","depth":2,"on_screen":true,"automation_id":"_NS:147","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Mute","depth":2,"on_screen":true,"automation_id":"_NS:151","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"0%","depth":2,"on_screen":true,"automation_id":"_NS:155","role_description":"text"},{"role":"AXRadioButton","text":"Change volume","depth":2,"on_screen":true,"automation_id":"_NS:164","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"100%","depth":2,"on_screen":true,"automation_id":"_NS:168","role_description":"text"},{"role":"AXStaticText","text":"200%","depth":2,"on_screen":true,"automation_id":"_NS:172","role_description":"text"},{"role":"AXStaticText","text":"03:36,22","depth":2,"on_screen":true,"role_description":"text"}]...
|
163510099187891368
|
-4946999708420828508
|
click
|
accessibility
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
03:36,22...
|
5362
|
NULL
|
NULL
|
NULL
|
|
5367
|
194
|
4
|
2026-05-07T15:19:04.790508+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167144790_m2.jpg...
|
CleanShot X
|
CleanShot
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
05:45,93...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Dimensions:","depth":2,"bounds":{"left":0.3879654,"top":0.54668796,"width":0.02825798,"height":0.012769354},"on_screen":true,"automation_id":"_NS:29","role_description":"text"},{"role":"AXStaticText","text":"Width:","depth":2,"bounds":{"left":0.39162233,"top":0.57701516,"width":0.024601065,"height":0.012769354},"on_screen":true,"automation_id":"_NS:67","role_description":"text"},{"role":"AXStaticText","text":"Height:","depth":2,"bounds":{"left":0.38962767,"top":0.6017558,"width":0.026595745,"height":0.012769354},"on_screen":true,"automation_id":"_NS:71","role_description":"text"},{"role":"AXTextField","text":"1812","depth":2,"bounds":{"left":0.41821808,"top":0.575419,"width":0.024933511,"height":0.016759777},"on_screen":true,"automation_id":"_NS:75","value":"1812","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXTextField","text":"1080","depth":2,"bounds":{"left":0.41821808,"top":0.6017558,"width":0.024933511,"height":0.016759777},"on_screen":true,"automation_id":"_NS:83","value":"1080","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Video quality:","depth":2,"bounds":{"left":0.38730052,"top":0.5139665,"width":0.028922873,"height":0.012769354},"on_screen":true,"automation_id":"_NS:86","role_description":"text"},{"role":"AXStaticText","text":"Ultra","depth":2,"bounds":{"left":0.47839096,"top":0.51476455,"width":0.018949468,"height":0.012769354},"on_screen":true,"automation_id":"_NS:94","role_description":"text"},{"role":"AXButton","text":"Trim & Convert","depth":1,"bounds":{"left":0.5787899,"top":0.66480446,"width":0.04155585,"height":0.025538707},"on_screen":true,"automation_id":"_NS:109","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Trim Only","depth":1,"bounds":{"left":0.54886967,"top":0.66480446,"width":0.030585106,"height":0.025538707},"on_screen":true,"automation_id":"_NS:114","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.37965426,"top":0.66480446,"width":0.025265958,"height":0.025538707},"on_screen":true,"automation_id":"_NS:118","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Estimated file size: ~1,66 GB","depth":1,"bounds":{"left":0.40990692,"top":0.669593,"width":0.05867686,"height":0.012769354},"on_screen":true,"automation_id":"_NS:123","role_description":"text"},{"role":"AXCheckBox","text":"play/pause","depth":1,"bounds":{"left":0.38464096,"top":0.43216282,"width":0.020611702,"height":0.050678372},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Audio:","depth":2,"bounds":{"left":0.50482047,"top":0.5123703,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:136","role_description":"text"},{"role":"AXRadioButton","text":"Convert to mono","depth":2,"bounds":{"left":0.53075135,"top":0.5339186,"width":0.04255319,"height":0.014365523},"on_screen":true,"automation_id":"_NS:140","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Don't change","depth":2,"bounds":{"left":0.53075135,"top":0.5123703,"width":0.035904255,"height":0.014365523},"on_screen":true,"automation_id":"_NS:147","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Mute","depth":2,"bounds":{"left":0.53075135,"top":0.5554669,"width":0.018949468,"height":0.014365523},"on_screen":true,"automation_id":"_NS:151","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"0%","depth":2,"bounds":{"left":0.53706783,"top":0.6113328,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:155","role_description":"text"},{"role":"AXRadioButton","text":"Change volume","depth":2,"bounds":{"left":0.53075135,"top":0.57701516,"width":0.039893616,"height":0.014365523},"on_screen":true,"automation_id":"_NS:164","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"100%","depth":2,"bounds":{"left":0.56200135,"top":0.612929,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:168","role_description":"text"},{"role":"AXStaticText","text":"200%","depth":2,"bounds":{"left":0.5859375,"top":0.6113328,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:172","role_description":"text"},{"role":"AXStaticText","text":"05:45,93","depth":2,"bounds":{"left":0.43018618,"top":0.4102155,"width":0.020279255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-1107769619310610462
|
-4946999708420812120
|
visual_change
|
accessibility
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
05:45,93...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
5368
|
194
|
5
|
2026-05-07T15:19:19.884232+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167159884_m2.jpg...
|
CleanShot X
|
CleanShot
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
06:09,43...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Dimensions:","depth":2,"bounds":{"left":0.3879654,"top":0.54668796,"width":0.02825798,"height":0.012769354},"on_screen":true,"automation_id":"_NS:29","role_description":"text"},{"role":"AXStaticText","text":"Width:","depth":2,"bounds":{"left":0.39162233,"top":0.57701516,"width":0.024601065,"height":0.012769354},"on_screen":true,"automation_id":"_NS:67","role_description":"text"},{"role":"AXStaticText","text":"Height:","depth":2,"bounds":{"left":0.38962767,"top":0.6017558,"width":0.026595745,"height":0.012769354},"on_screen":true,"automation_id":"_NS:71","role_description":"text"},{"role":"AXTextField","text":"1812","depth":2,"bounds":{"left":0.41821808,"top":0.575419,"width":0.024933511,"height":0.016759777},"on_screen":true,"automation_id":"_NS:75","value":"1812","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXTextField","text":"1080","depth":2,"bounds":{"left":0.41821808,"top":0.6017558,"width":0.024933511,"height":0.016759777},"on_screen":true,"automation_id":"_NS:83","value":"1080","role_description":"text field","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Video quality:","depth":2,"bounds":{"left":0.38730052,"top":0.5139665,"width":0.028922873,"height":0.012769354},"on_screen":true,"automation_id":"_NS:86","role_description":"text"},{"role":"AXStaticText","text":"Ultra","depth":2,"bounds":{"left":0.47839096,"top":0.51476455,"width":0.018949468,"height":0.012769354},"on_screen":true,"automation_id":"_NS:94","role_description":"text"},{"role":"AXButton","text":"Trim & Convert","depth":1,"bounds":{"left":0.5787899,"top":0.66480446,"width":0.04155585,"height":0.025538707},"on_screen":true,"automation_id":"_NS:109","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Trim Only","depth":1,"bounds":{"left":0.54886967,"top":0.66480446,"width":0.030585106,"height":0.025538707},"on_screen":true,"automation_id":"_NS:114","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.37965426,"top":0.66480446,"width":0.025265958,"height":0.025538707},"on_screen":true,"automation_id":"_NS:118","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Estimated file size: ~1,66 GB","depth":1,"bounds":{"left":0.40990692,"top":0.669593,"width":0.05867686,"height":0.012769354},"on_screen":true,"automation_id":"_NS:123","role_description":"text"},{"role":"AXCheckBox","text":"play/pause","depth":1,"bounds":{"left":0.38464096,"top":0.43216282,"width":0.020611702,"height":0.050678372},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"Audio:","depth":2,"bounds":{"left":0.50482047,"top":0.5123703,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:136","role_description":"text"},{"role":"AXRadioButton","text":"Convert to mono","depth":2,"bounds":{"left":0.53075135,"top":0.5339186,"width":0.04255319,"height":0.014365523},"on_screen":true,"automation_id":"_NS:140","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Don't change","depth":2,"bounds":{"left":0.53075135,"top":0.5123703,"width":0.035904255,"height":0.014365523},"on_screen":true,"automation_id":"_NS:147","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXRadioButton","text":"Mute","depth":2,"bounds":{"left":0.53075135,"top":0.5554669,"width":0.018949468,"height":0.014365523},"on_screen":true,"automation_id":"_NS:151","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"0%","depth":2,"bounds":{"left":0.53706783,"top":0.6113328,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:155","role_description":"text"},{"role":"AXRadioButton","text":"Change volume","depth":2,"bounds":{"left":0.53075135,"top":0.57701516,"width":0.039893616,"height":0.014365523},"on_screen":true,"automation_id":"_NS:164","role_description":"radio button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"100%","depth":2,"bounds":{"left":0.56200135,"top":0.612929,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:168","role_description":"text"},{"role":"AXStaticText","text":"200%","depth":2,"bounds":{"left":0.5859375,"top":0.6113328,"width":0.022938829,"height":0.012769354},"on_screen":true,"automation_id":"_NS:172","role_description":"text"},{"role":"AXStaticText","text":"06:09,43","depth":2,"bounds":{"left":0.44614363,"top":0.4102155,"width":0.020279255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
2507216868760114952
|
-4947017438045810036
|
visual_change
|
accessibility
|
NULL
|
Dimensions:
Width:
Height:
1812
1080
Video quality Dimensions:
Width:
Height:
1812
1080
Video quality:
Ultra
Trim & Convert
Trim Only
Cancel
Estimated file size: ~1,66 GB
play/pause
Audio:
Convert to mono
Don't change
Mute
0%
Change volume
100%
200%
06:09,43...
|
5367
|
NULL
|
NULL
|
NULL
|
|
5372
|
194
|
7
|
2026-05-07T15:19:47.764286+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167187764_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Do you want to replace the existing video?
Replace Do you want to replace the existing video?
Replace Video...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Do you want to replace the existing video?","depth":1,"bounds":{"left":0.46276596,"top":0.38387868,"width":0.07446808,"height":0.025538707},"on_screen":true,"lines":[{"char_start":0,"char_count":27,"bounds":{"left":0.47146508,"top":0.38387868,"width":0.05815871,"height":0.012769354}},{"char_start":27,"char_count":15,"bounds":{"left":0.4838982,"top":0.39664805,"width":0.032203592,"height":0.012769354}}],"automation_id":"_NS:78","role_description":"text"},{"role":"AXButton","text":"Replace Video","depth":1,"bounds":{"left":0.46010637,"top":0.41739824,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button-1","role_description":"button","is_enabled":true,"is_focused":false}]...
|
-3907137029581090506
|
2596079378333100210
|
click
|
hybrid
|
NULL
|
Do you want to replace the existing video?
Replace Do you want to replace the existing video?
Replace Video
ActivityMoreCleanShot XJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-ofhce# support# thank-vous# the people of jimi..o- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka StoyanovaFal Ves. Galya Dimitrova DAneliva Angelova6 Vasil Vasilev&o James Grahamed Nikolay IvanovLukas Kovalik y...::: ADoS-T lira Cloud• Toast> ih External Librariesv E Scratches and Consolesv O Database ConsolesV AEUA console 1EU.A DEAL RISKS (EU1#EU EUA console fiminnvGlocalho: 1891a 29m 20c4 Di giminny@localhostTrim & encode video (*E) iminny@localhc 187#SE fliminnvAlocalhost]A zoho dev fiiminnvGlocalh 190Q Describe what you are looking forw releases9 22MessagesO Deplos( Files& BookmarksProject:appWhen.05/07/2026 14:29:40lag.View JobCircleCl APP 6:18 PMNew commits deployed to Prophet P[URL_WITH_CREDENTIALS] Token refresh and retry successful'.l'team id' => $client->getConfiq(->qetTeam©->qetIdo.Plattorm Sprint s @2 - Plattorm XSevenShores\Hubspot\ExceptionsService-0 Jy 2080A Illumina•• Pull rec1 Useroild• JY-2077ProblenDo Search :8 JiminnyL Now TaJ JIMINNYg For youinny.aulasslan.netlfa/sorware/c/loroecis/cy/ooarass.• Search|spaces Jiminny (New)4100Video quality:Dimensions1812 x 1080 Oriaina)]Width. 1912Height:1080|Estimated file size: ~1.4 GB-1 UltraAudio:Don't changea Convert to mono• MuteChange volumeTrim OnivAJ Panorama for CallScorina in ODAUTOMATED AI SCORINGIIn Dev2.5=JY-20361[HubSpot] OptimiseCRM rematchina ondelete hubsoot..PLATFORM STABILITYTn NoulGOWOOA100% s2•Thu 7 May 18:19:47+ CreateAsk RovoActive sprintsCalendar • Reports 4 Testing Board W List Forms Components %> Development% CodeMore 8Eoic vType vQuick filters vComplete sprintGrouo: @ueriesICODE REVIEWBLOCKEDQA 1|PO ACCEPTANCEDEPLOY 7rageSync opportunitiesIND FAS...owner (user_id is..Al Reports > Emptypage design andpromotionPLATFORM STABILITYAJ REPORTSIn OAlDenloved|3 88 0000 =1 12 •000 =JY-20352•0 JY-20372Grok via AzurelMAINTENANCEDeplovedœe=0 JY-20726Allow ticorc to dolotaSS and Panoramapromots when thos...AJ REPORTSDeployed1 8000·½ JY-20770Release AJPanorama reports tocustomersA.I REpORTSDeniovedt0.5 72 0000=O JY-20740Wrona formattina fosummary in the CRMMAINTENANCEDeployed3Ý•00=...
|
5370
|
NULL
|
NULL
|
NULL
|
|
5371
|
193
|
5
|
2026-05-07T15:19:47.858083+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167187858_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Do you want to replace the existing video?
Replace Do you want to replace the existing video?
Replace Video
Save as New Video
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Do you want to replace the existing video?","depth":1,"on_screen":true,"automation_id":"_NS:78","role_description":"text"},{"role":"AXButton","text":"Replace Video","depth":1,"on_screen":true,"automation_id":"action-button-1","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Save as New Video","depth":1,"on_screen":true,"automation_id":"action-button-2","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"automation_id":"action-button-3","role_description":"button","is_enabled":true,"is_focused":true}]...
|
-1122987962102006611
|
4433720126682620066
|
click
|
hybrid
|
NULL
|
Do you want to replace the existing video?
Replace Do you want to replace the existing video?
Replace Video
Save as New Video
Cancel
Firefox FileEditView€ <→ C= [1 Google MeetHistoryBookmarksProfilesToolsWindow Help= meet.google.com/landing?authuser=lukas.kovalik@jiminny.com1819476:19 PM • Thu, May 7+Meetings0 CallsSecure video conferencingfor everyoneConnect, collaborate, and celebrate from anywhere withGoogle MeetEk New meetingEnter a code or nicknameJoinGet a link you can shareClick New meeting to get a link you can send to peopleyou want to meet with...
|
5369
|
NULL
|
NULL
|
NULL
|
|
5373
|
194
|
8
|
2026-05-07T15:19:50.265740+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778167190265_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Do you want to replace the existing video?
Replace Do you want to replace the existing video?
Replace Video
Save as New Video
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Do you want to replace the existing video?","depth":1,"bounds":{"left":0.46276596,"top":0.38387868,"width":0.07446808,"height":0.025538707},"on_screen":true,"lines":[{"char_start":0,"char_count":27,"bounds":{"left":0.47146508,"top":0.38387868,"width":0.05815871,"height":0.012769354}},{"char_start":27,"char_count":15,"bounds":{"left":0.4838982,"top":0.39664805,"width":0.032203592,"height":0.012769354}}],"automation_id":"_NS:78","role_description":"text"},{"role":"AXButton","text":"Replace Video","depth":1,"bounds":{"left":0.46010637,"top":0.41739824,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button-1","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Save as New Video","depth":1,"bounds":{"left":0.46010637,"top":0.4445331,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button-2","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.46010637,"top":0.47964883,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button-3","role_description":"button","is_enabled":true,"is_focused":true}]...
|
-1122987962102006611
|
4433720126682620066
|
visual_change
|
hybrid
|
NULL
|
Do you want to replace the existing video?
Replace Do you want to replace the existing video?
Replace Video
Save as New Video
Cancel
ActivityMoreCleanShot XJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-ofhce# support# thank-vous# the people of jimi..o- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka StoyanovaFal Ves. Galya Dimitrova DAneliva Angelova6 Vasil Vasilev&o James Grahamed Nikolay IvanovLukas Kovalik y...::: ADoS-T lira Cloud• Toast> ih External Librariesv E Scratches and Consolesv O Database ConsolesV AEUA console 1EU.A DEAL RISKS (EU1#EU EUA console fiminnvGlocalho: 1891a 29m 20c4 Di giminny@localhostTrim & encode video (*E) iminny@localhc 187#SE fliminnvAlocalhost]A zoho dev fiiminnvGlocalh 190Q Describe what you are looking forw releases8 22MessagesO Deplos( Files& BookmarksProject:appWhen.05/07/2026 14:29:40lag.View JobCircleCl APP 6:18 PMNew commits deployed to Prophet P[URL_WITH_CREDENTIALS] Token refresh and retry successful'.l'team id' => $client->getConfiq(->qetTeam©->qetIdo.Plattorm Sprint s @2 - Plattorm XSevenShores\Hubspot\ExceptionsService-0 Jy 2080A Illumina•• Pull recU Useroil( JY-2071ProblenDo Search :8 JiminnyL Now Talg For youminny.aulasslan.netlfa/sorware/c/loroecis/cy/ooarass.J JIMINNY• Search|spaces Jiminny (NewGOWOOA100% s2•Thu 7 May 18:19:50+ CreateAsk RovoVideo quality:Dimensions1812 x 1080 (Original)Do you wisting vreplace theReplace VideoSave as New VideoCancellon't change• Convert to moncMuteChance volumeEstimated file size: ~1.4 GBAJ Panorama for CallScorina in ODAUTOMATED AI SCORINGIIn Dev2.5=JY-20361[HubSpot] OptimiseCRM rematchina ondelete hubsoot..PLATFORM STABILITYTn Noul• Active sprintsCalendar • Reports 4 Testing Board W List Forms Components %> Development% CodeMore 8Eoic vType vQuick filters vComplete sprintGrouo: @ueriesICODE REVIEWBLOCKEDQA 1|PO ACCEPTANCEDEPLOY 7Sync opportunitiesIND FAS...owner (user_id is..Al Reports > Emptypage design andpromotionPLATFORM STABILITYAJ REPORTSIn OAlDenloved|3 88 0000 =1 12 •000 =JY-20352•0 JY-20372Grok via AzurelMAINTENANCEDeplovedœe=0 JY-20726Allow ticorc to dolotaSS and Panoramapromots when thos...AJ REPORTSDeployed1 8000·½ JY-20770Release AJPanorama reports tocustomersA.I REpORTSDeniovedt0.5 72 0000=O JY-20740Wrona formattina fosummary in the CRMMAINTENANCEDeployed3Ý•00=...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
21043
|
919
|
22
|
2026-05-11T17:04:59.449821+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778519099449_m2.jpg...
|
Code
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Do you want to save the changes you made to Client Do you want to save the changes you made to Client.php?
Your changes will be lost if you don't save them.
Save
Don't Save
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Do you want to save the changes you made to Client.php?","depth":1,"bounds":{"left":0.46276596,"top":0.46847567,"width":0.07446808,"height":0.025538707},"on_screen":true,"lines":[{"char_start":0,"char_count":32,"bounds":{"left":0.46490273,"top":0.46847567,"width":0.07100154,"height":0.012769354}},{"char_start":32,"char_count":23,"bounds":{"left":0.47394413,"top":0.481245,"width":0.052111767,"height":0.012769354}}],"automation_id":"_NS:78","role_description":"text"},{"role":"AXStaticText","text":"Your changes will be lost if you don't save them.","depth":1,"bounds":{"left":0.46276596,"top":0.5019952,"width":0.07446808,"height":0.022346368},"on_screen":true,"lines":[{"char_start":0,"char_count":39,"bounds":{"left":0.468002,"top":0.5019952,"width":0.065045945,"height":0.011173184}},{"char_start":39,"char_count":10,"bounds":{"left":0.49053004,"top":0.5131684,"width":0.01893989,"height":0.011173184}}],"automation_id":"_NS:58","role_description":"text"},{"role":"AXButton","text":"Save","depth":1,"bounds":{"left":0.46010637,"top":0.5323224,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button--999","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Don't Save","depth":1,"bounds":{"left":0.46010637,"top":0.5594573,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button--997","role_description":"button","is_enabled":true,"is_focused":true},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.46010637,"top":0.594573,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button--998","role_description":"button","is_enabled":true,"is_focused":false}]...
|
-8944230292389449886
|
-8993375785305258178
|
visual_change
|
accessibility
|
NULL
|
Do you want to save the changes you made to Client Do you want to save the changes you made to Client.php?
Your changes will be lost if you don't save them.
Save
Don't Save
Cancel...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
10392
|
471
|
30
|
2026-05-08T17:21:18.372777+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778260878372_m1.jpg...
|
Code
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Do you want to save the changes you made to screen Do you want to save the changes you made to screenpipe_sync.sh?
Your changes will be lost if you don't save them.
Save
Don't Save
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Do you want to save the changes you made to screenpipe_sync.sh?","depth":1,"on_screen":true,"automation_id":"_NS:78","role_description":"text"},{"role":"AXStaticText","text":"Your changes will be lost if you don't save them.","depth":1,"on_screen":true,"automation_id":"_NS:58","role_description":"text"},{"role":"AXButton","text":"Save","depth":1,"on_screen":true,"automation_id":"action-button--999","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Don't Save","depth":1,"on_screen":true,"automation_id":"action-button--997","role_description":"button","is_enabled":true,"is_focused":true},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"automation_id":"action-button--998","role_description":"button","is_enabled":true,"is_focused":false}]...
|
7580227543850059871
|
-4524608763581708516
|
click
|
accessibility
|
NULL
|
Do you want to save the changes you made to screen Do you want to save the changes you made to screenpipe_sync.sh?
Your changes will be lost if you don't save them.
Save
Don't Save
Cancel...
|
10390
|
NULL
|
NULL
|
NULL
|
|
10394
|
472
|
32
|
2026-05-08T17:21:21.126765+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778260881126_m2.jpg...
|
Code
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Do you want to save the changes you made to screen Do you want to save the changes you made to screenpipe_sync.sh?
Your changes will be lost if you don't save them.
Save
Don't Save
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Do you want to save the changes you made to screenpipe_sync.sh?","depth":1,"bounds":{"left":0.46276596,"top":0.46209097,"width":0.07446808,"height":0.03830806},"on_screen":true,"lines":[{"char_start":0,"char_count":32,"bounds":{"left":0.46490273,"top":0.46209097,"width":0.07100154,"height":0.012769354}},{"char_start":32,"char_count":12,"bounds":{"left":0.48687103,"top":0.47486034,"width":0.027346835,"height":0.012769354}},{"char_start":44,"char_count":19,"bounds":{"left":0.4772224,"top":0.48762968,"width":0.045555197,"height":0.012769354}}],"automation_id":"_NS:78","role_description":"text"},{"role":"AXStaticText","text":"Your changes will be lost if you don't save them.","depth":1,"bounds":{"left":0.46276596,"top":0.5083799,"width":0.07446808,"height":0.022346368},"on_screen":true,"lines":[{"char_start":0,"char_count":39,"bounds":{"left":0.468002,"top":0.5083799,"width":0.065045945,"height":0.011173184}},{"char_start":39,"char_count":10,"bounds":{"left":0.49053004,"top":0.51955307,"width":0.01893989,"height":0.011173184}}],"automation_id":"_NS:58","role_description":"text"},{"role":"AXButton","text":"Save","depth":1,"bounds":{"left":0.46010637,"top":0.5387071,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button--999","role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"Don't Save","depth":1,"bounds":{"left":0.46010637,"top":0.565842,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button--997","role_description":"button","is_enabled":true,"is_focused":true},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.46010637,"top":0.6009577,"width":0.07978723,"height":0.031923383},"on_screen":true,"automation_id":"action-button--998","role_description":"button","is_enabled":true,"is_focused":false}]...
|
7580227543850059871
|
-4524608763581708516
|
visual_change
|
accessibility
|
NULL
|
Do you want to save the changes you made to screen Do you want to save the changes you made to screenpipe_sync.sh?
Your changes will be lost if you don't save them.
Save
Don't Save
Cancel...
|
10393
|
NULL
|
NULL
|
NULL
|
|
14931
|
671
|
5
|
2026-05-11T06:06:33.148048+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778479593148_m2.jpg...
|
Notion Calendar
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Docker DeSKLOpCaltVIeww Q0, Chat:= Cowork‹ Code+ N Docker DeSKLOpCaltVIeww Q0, Chat:= Cowork‹ Code+ New chatã Projectso0 Arutacts₴ CustomizeBu garian cit zenshio apolication proces:Dawarich location tracking projectScreenpipe sync script talling arter recelMonthly expense trackingExporting transaction data from Notion® How much have I spent for groc...April 2026 spending by categoryCode diff reviewHuospot rate limit imolementation strateScreenpipe retention policy code locatio!Viewing retention policy in screenpipeClean shot x video recording terminationHubSpot rate limit handling with executeUntitled@ Screen pine. Is there ability…SMB mount access inconsistencv betweWhat is the best switch I can….Permission denied on screenpipe volumeScreenpipe sync database attachment erLast swimming outing with Dan.Chromecast remote volume buttons notilK Lukas. ProMay 2026 Week20Mon (11Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Michelle Weston (PTO - 5 davs))Mira Lenkova (PTO - 3 davs)Tue 12Plattorm Office Daynl docker.desktop PERSONALThu 14(James Graham (PTO - 2 days)Liz Mulraney (PTO - 1 day)Fri15Q Search* Coffee and Claude timeHow can I help you today?& WriteLearn" Code• Life stuffOpus 4.7 AdQ Claude's c05:0006:00|08:0010:00 Daily11:0012:00|Starting the Docker Engine...Docker Engine is the underlying technology that runs containersRAMO00 GR CPUJ0.00% Dick•.. GR used (limit • •GR)15:00|17.0018:00SatloDally - Platorm • In 3ynWeek vTodaySun 17Sign in.• мon 11 May 9:00.34-a Search eventsDallv - Plattorm09:45-10:05 20minJoin Google MeetUseful shortcutsMenu bar calendarcontrol 2KTogale sidebarGo to dateAll keyboard shortcuts›_ © Update availableGet Calendar to go...
|
NULL
|
-2146737449787920009
|
NULL
|
click
|
ocr
|
NULL
|
Docker DeSKLOpCaltVIeww Q0, Chat:= Cowork‹ Code+ N Docker DeSKLOpCaltVIeww Q0, Chat:= Cowork‹ Code+ New chatã Projectso0 Arutacts₴ CustomizeBu garian cit zenshio apolication proces:Dawarich location tracking projectScreenpipe sync script talling arter recelMonthly expense trackingExporting transaction data from Notion® How much have I spent for groc...April 2026 spending by categoryCode diff reviewHuospot rate limit imolementation strateScreenpipe retention policy code locatio!Viewing retention policy in screenpipeClean shot x video recording terminationHubSpot rate limit handling with executeUntitled@ Screen pine. Is there ability…SMB mount access inconsistencv betweWhat is the best switch I can….Permission denied on screenpipe volumeScreenpipe sync database attachment erLast swimming outing with Dan.Chromecast remote volume buttons notilK Lukas. ProMay 2026 Week20Mon (11Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Michelle Weston (PTO - 5 davs))Mira Lenkova (PTO - 3 davs)Tue 12Plattorm Office Daynl docker.desktop PERSONALThu 14(James Graham (PTO - 2 days)Liz Mulraney (PTO - 1 day)Fri15Q Search* Coffee and Claude timeHow can I help you today?& WriteLearn" Code• Life stuffOpus 4.7 AdQ Claude's c05:0006:00|08:0010:00 Daily11:0012:00|Starting the Docker Engine...Docker Engine is the underlying technology that runs containersRAMO00 GR CPUJ0.00% Dick•.. GR used (limit • •GR)15:00|17.0018:00SatloDally - Platorm • In 3ynWeek vTodaySun 17Sign in.• мon 11 May 9:00.34-a Search eventsDallv - Plattorm09:45-10:05 20minJoin Google MeetUseful shortcutsMenu bar calendarcontrol 2KTogale sidebarGo to dateAll keyboard shortcuts›_ © Update availableGet Calendar to go...
|
14929
|
NULL
|
NULL
|
NULL
|
|
7439
|
332
|
10
|
2026-05-08T08:49:27.811652+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778230167811_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Docker DesKLopcaltProledey• D Datadogo clientonp© Docker DesKLopcaltProledey• D Datadogo clientonp© Constants.php• _ Datelime_ Dealinsignts_DealKISKSD ElasticSearchencodingEncrvptionD ES_ FeatureFlagsU FFMpeg1 FileSvstemN GeckoGongD GuzzleHttpM KevPointeiM KiockM l anquadeDetectionMliveFoedMlocks1M Math1M Media PinelineiN MootinaRotMobileSettingsM Modal0 Notification_ Nudae_ ParagraphBreaker_ ParticipantspeechPartitionedCookieaPlavista Propheta Prophet All/a ProsperWorksQueuev llobCPateLimitAware.ohtC) Patelimit AwareWral@ RotsQueueConstente, 46© Constants.php(C) ProceccinaQueueConsM RouteSam|IN CAMi CoodorM ContnM Corinlizor© MatchActivityCrmData.php© CrmActivityService.phpO Automatedkeponkesultonp© Constants.php X© CheckAndRetryRemoteMatch.phpRateLimitexception.php© SyncOpportunities.Job.phpOpportunitysynclrait.ondImportBatchJobTrait.php(c) [EMAIL]) HydrateCrmDataByExternalCallidJob.php© ConferenceCrmMatcherJob.php(C) MatchCrmData.phpC) Activity.php(C) DeraultUpdateCrmDataResolver.ohp(C) CachedCrmServiceDecorator.phofinal class Constantsт X.8public const string CUSTOMER_API = 'jiminny.customer_api';lusdgepublic const string ACTIVITY_LANGUAGE_DETECTION_SUCCESS = 'jiminny.activity.language_detection.success';Lusagespublic const string ACTIVITY_LANGUAGE_DETECTION_FAIL = 'jiminny.activity.language_detection.fail':Lusagespublic const string ASK_JIMINNY_DEAL_TEAM_REQUEST = 'jiminny.ask_jiminny_deals.team_write_requests':1usagepublic const string ASK_JIMINNY_DEAL_TEAM_READ_REQUEST = 'jiminny.ask_jiminny_deals.team_read_requests':Z usagespublic const string CRM_AUTO_SYNC = 'jiminny.crm.auto-sync':public const string ACTIVITIES DIALERS STATS = 'jiminny.activities.dialers.stats':public const string ACTIVITIES DIALERS REASON STATS = 'jiminny.activities.dialers.reason.stats':public const string ACTIVITIES DIALERS TRACK IMPORTED = 'jiminny.activities.dialers.track.imported':public const string ACTIVITTES DIALERS TRACK DURATION = 1jiminnv.activities.dialers.track.duration'•public const string AT AUTOMATION CRM WRITE EXECUTED = 'jiminnv.ai automation.com update executed'•public const string AUTOMATED_REPORTS = 'Jiminny.automated_reports.usage';public const string ASK_JIMINNY_REPORTS = 'Jiminny.ask_jiminny_reports.usage';1 usaaenuhiaic const strino HUBSPot WEshoOk SYNe E "iminnv.crm.hubsnot wehhook svnc!.public const string AI_ACTIVITY_TYPE = 'jiminny.ai_activity_type';public const string AI_CALL_SCORING = 'jiminny.ai_call_scoring';3 usagespublic const string WEBHOOK_DELIVERY_SUCCESS = 'jiminny.webhook.delivery.success';4 usagespublic const string WEBHOOK_DELIVERY_FAILED = 'jiminny.webhook.delivery.failed':s usagespublic const string ACTIVITY_PROCESSING STEP TIME = 'jiminny.activity.processing.step.time':© ProviderRateLimiter.php= custom.logElaravel.log4 SF jiminny@localhost]A HS_local jiminny@localhost]tiò accounts jiminny@localhost]# console [Pkol)# console [eu)A console [STAGING] X• Tx: Auto vPlavaroundSo jiminny020 A18 V13 ^# id, uvid, type, provider, playbook_category_id, user_id, lead_id, contact_id, account_id, opportunity.# crm contiquration1d, crm provider 1d, transcription 1d, statusfrom activities where crm_configuration_id = 1 and type = 'conference"*and crm orovider 1d is NOT NULLIi docker desktoo PERSONALAsk Gordon BETAcontainersGive feedbackContainersImagesContainer CPU usage OVolumesIo contalners are runnino.KubernetesBuilds0 SearchiModelsNameContainer IDMCP Toolkit BETAredis1220ffe7ed?7Dockor HuhД AI 7c3ес7911304Docker scoutA AM 00a86edb2f8dExtensionsblackfire.f3fa652b7054Manaaeuminny ext- 68/5460803e0elasticsearch e802ad473a4fV Resource usagedatadod-10727542fa222SearchContainer memory usage ONo containers are runnindOnlv show runnina containersImagePort(s)redis:56370-6379kibana/kibana:7.10.2 5601:5601ЛАИА-ЛЛИЛwernicnvnolokblackfire/blackfire:1.: 8707:87079200:9200Chaw all narte 2)datadoalaaent:6.121CPU (%) ActionsN/AIN/AN/AN/AN/ATN/Amariach.167064070/202mariadb:11.4.5220You are signed outSian in to share images andcolllahorate with vour teamRAM 047 GR CP|J2 81% Dick•.. GR used (limit • •GR)SELECT * FROM automated_reports WHERE uuid_to_bin('18a06a75-afd2-476f-aadc-14d4057bdda2') = uuid;SELECT * FROM automated_report_results WHERE uuid_to_bin('582d4b50-8cd3-42a9-9819-d676ff8f3b43') = uuid;supoont Dally• In sn 11m100% 2Fri 8 May 11:49:28CascadeHubspot Rate LimitingHubSpot Rate Limit HDatadog Metric Condr+0 ..atedReport->isAsk liminnvReport( we should send it taThought for 1s >Read AutomatedReportsCallbackService.phpThoughts >Sign inloa method:Show charts11m1nny.ask 11m1nny_reports.usage';ortsCallbackService.php:67-91 - Updated pushToDatadog() method to:() returns trueno report_type or media_type )led reportsGal .Sian in>_ ® Update available-575577Ask anvthina (&4DC° Adantivefo 4 spaces...
|
NULL
|
-4980751433763654519
|
NULL
|
visual_change
|
ocr
|
NULL
|
Docker DesKLopcaltProledey• D Datadogo clientonp© Docker DesKLopcaltProledey• D Datadogo clientonp© Constants.php• _ Datelime_ Dealinsignts_DealKISKSD ElasticSearchencodingEncrvptionD ES_ FeatureFlagsU FFMpeg1 FileSvstemN GeckoGongD GuzzleHttpM KevPointeiM KiockM l anquadeDetectionMliveFoedMlocks1M Math1M Media PinelineiN MootinaRotMobileSettingsM Modal0 Notification_ Nudae_ ParagraphBreaker_ ParticipantspeechPartitionedCookieaPlavista Propheta Prophet All/a ProsperWorksQueuev llobCPateLimitAware.ohtC) Patelimit AwareWral@ RotsQueueConstente, 46© Constants.php(C) ProceccinaQueueConsM RouteSam|IN CAMi CoodorM ContnM Corinlizor© MatchActivityCrmData.php© CrmActivityService.phpO Automatedkeponkesultonp© Constants.php X© CheckAndRetryRemoteMatch.phpRateLimitexception.php© SyncOpportunities.Job.phpOpportunitysynclrait.ondImportBatchJobTrait.php(c) [EMAIL]) HydrateCrmDataByExternalCallidJob.php© ConferenceCrmMatcherJob.php(C) MatchCrmData.phpC) Activity.php(C) DeraultUpdateCrmDataResolver.ohp(C) CachedCrmServiceDecorator.phofinal class Constantsт X.8public const string CUSTOMER_API = 'jiminny.customer_api';lusdgepublic const string ACTIVITY_LANGUAGE_DETECTION_SUCCESS = 'jiminny.activity.language_detection.success';Lusagespublic const string ACTIVITY_LANGUAGE_DETECTION_FAIL = 'jiminny.activity.language_detection.fail':Lusagespublic const string ASK_JIMINNY_DEAL_TEAM_REQUEST = 'jiminny.ask_jiminny_deals.team_write_requests':1usagepublic const string ASK_JIMINNY_DEAL_TEAM_READ_REQUEST = 'jiminny.ask_jiminny_deals.team_read_requests':Z usagespublic const string CRM_AUTO_SYNC = 'jiminny.crm.auto-sync':public const string ACTIVITIES DIALERS STATS = 'jiminny.activities.dialers.stats':public const string ACTIVITIES DIALERS REASON STATS = 'jiminny.activities.dialers.reason.stats':public const string ACTIVITIES DIALERS TRACK IMPORTED = 'jiminny.activities.dialers.track.imported':public const string ACTIVITTES DIALERS TRACK DURATION = 1jiminnv.activities.dialers.track.duration'•public const string AT AUTOMATION CRM WRITE EXECUTED = 'jiminnv.ai automation.com update executed'•public const string AUTOMATED_REPORTS = 'Jiminny.automated_reports.usage';public const string ASK_JIMINNY_REPORTS = 'Jiminny.ask_jiminny_reports.usage';1 usaaenuhiaic const strino HUBSPot WEshoOk SYNe E "iminnv.crm.hubsnot wehhook svnc!.public const string AI_ACTIVITY_TYPE = 'jiminny.ai_activity_type';public const string AI_CALL_SCORING = 'jiminny.ai_call_scoring';3 usagespublic const string WEBHOOK_DELIVERY_SUCCESS = 'jiminny.webhook.delivery.success';4 usagespublic const string WEBHOOK_DELIVERY_FAILED = 'jiminny.webhook.delivery.failed':s usagespublic const string ACTIVITY_PROCESSING STEP TIME = 'jiminny.activity.processing.step.time':© ProviderRateLimiter.php= custom.logElaravel.log4 SF jiminny@localhost]A HS_local jiminny@localhost]tiò accounts jiminny@localhost]# console [Pkol)# console [eu)A console [STAGING] X• Tx: Auto vPlavaroundSo jiminny020 A18 V13 ^# id, uvid, type, provider, playbook_category_id, user_id, lead_id, contact_id, account_id, opportunity.# crm contiquration1d, crm provider 1d, transcription 1d, statusfrom activities where crm_configuration_id = 1 and type = 'conference"*and crm orovider 1d is NOT NULLIi docker desktoo PERSONALAsk Gordon BETAcontainersGive feedbackContainersImagesContainer CPU usage OVolumesIo contalners are runnino.KubernetesBuilds0 SearchiModelsNameContainer IDMCP Toolkit BETAredis1220ffe7ed?7Dockor HuhД AI 7c3ес7911304Docker scoutA AM 00a86edb2f8dExtensionsblackfire.f3fa652b7054Manaaeuminny ext- 68/5460803e0elasticsearch e802ad473a4fV Resource usagedatadod-10727542fa222SearchContainer memory usage ONo containers are runnindOnlv show runnina containersImagePort(s)redis:56370-6379kibana/kibana:7.10.2 5601:5601ЛАИА-ЛЛИЛwernicnvnolokblackfire/blackfire:1.: 8707:87079200:9200Chaw all narte 2)datadoalaaent:6.121CPU (%) ActionsN/AIN/AN/AN/AN/ATN/Amariach.167064070/202mariadb:11.4.5220You are signed outSian in to share images andcolllahorate with vour teamRAM 047 GR CP|J2 81% Dick•.. GR used (limit • •GR)SELECT * FROM automated_reports WHERE uuid_to_bin('18a06a75-afd2-476f-aadc-14d4057bdda2') = uuid;SELECT * FROM automated_report_results WHERE uuid_to_bin('582d4b50-8cd3-42a9-9819-d676ff8f3b43') = uuid;supoont Dally• In sn 11m100% 2Fri 8 May 11:49:28CascadeHubspot Rate LimitingHubSpot Rate Limit HDatadog Metric Condr+0 ..atedReport->isAsk liminnvReport( we should send it taThought for 1s >Read AutomatedReportsCallbackService.phpThoughts >Sign inloa method:Show charts11m1nny.ask 11m1nny_reports.usage';ortsCallbackService.php:67-91 - Updated pushToDatadog() method to:() returns trueno report_type or media_type )led reportsGal .Sian in>_ ® Update available-575577Ask anvthina (&4DC° Adantivefo 4 spaces...
|
7438
|
NULL
|
NULL
|
NULL
|
|
7438
|
332
|
9
|
2026-05-08T08:49:24.881222+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778230164881_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Docker DesktopcaltProleteyD Datadogo clientonp© Co Docker DesktopcaltProleteyD Datadogo clientonp© Constants.php• _ Datelime_ Dealinsignts_DealKISKS• ElasticSearchEncodingEncrvptionC ES_ FeatureFlags0 FFMpeg• FileSystemN Gecko• GongD GuzzleHttp• KeyPointsiM Kiock• LanguageDetection• LiveFeedD LocksD Math• MediaPipelineMeetingBotMobileSettingsM ModalD Notification_ Nudae_ ParagraphBreaker_ ParticipantspeechPartitionedCookieaPlavistPropheta Prophet All/a ProsperWorksQueuev OJobC PateLimitAware.omt©RateLimitAwareWra@ RotsQueueConstante, 46© Constants.php(C) ProceccinaQueueConsRouterSam|D SCIMD SeederD SentryD Serializer©MatchActivityCrmData.php© CrmActivityService.phpO Automatedkeponkesultonp© Constants.phpx© CheckAndRetryRemoteMatch.phpRateLimitexception.php© SyncOpportunitiesJob.phpOpportunitysynclrait.ondImportBatchJobTrait.php(c)SyncHubspotObiects.phpC) HydrateCrmDataByExternalCallidJob.php© ConferenceCrmMatcherJob.php© MatchCrmData.php© Activity.php(C) DeraultUpdateCrmDataResolver.ohp© CachedCrmServiceDecorator.phpfinal class Constantsm| x8public const string CUSTOMER_API = 'jiminny.customer_api';lusdgeLusagesLusages1usageZ usagespublic const string ACTIVITY_LANGUAGE_DETECTION_SUCCESS = 'jiminny.activity.language_detection.success';public const string ACTIVITY_LANGUAGE_DETECTION_FAIL = 'jiminny.activity.language_detection.fail';public const string ASK_JIMINNY_DEAL_TEAM_REQUEST = 'jiminny.ask_jiminny_deals.team_write_requests';public const string ASK_JIMINNY_DEAL_TEAM_READ_REQUEST = 'jiminny.ask_jiminny_deals.team_read_requests';public const string CRM_AUTO_SYNC = 'jiminny.crm.auto-sync';public const string ACTIVITIES_DIALERS_STATS = 'jiminny.activities.dialers.stats';public const string ACTIVITIES_DIALERS_REASON_STATS = 'jiminny.activities.dialers.reason.stats';public const string ACTIVITIES_DIALERS_TRACK_IMPORTED = 'jiminny.activities.dialers.track.imported';public const string ACTIVITIES_QIALERS_TRACK_DURATION = 'jiminny.activities.dialers.track.duration';public const string AI_AUTOMATION_CRM_WRITE_EXECUTED = 'jiminny.ai_automation.crm_update_executed';public const string AUTOMATED_REPORTS = 'jiminny.automated_reports.usage';public const string ASK_LIMINNY_REPORTS = "jiminny.ask_jiminny_reports.usage';1 usagepublic const string HUBSPOT_WEBHOOK_SYNC = 'jiminny.crm.hubspot_webhook_sync';public const string AI_ACTIVITY_TYPE = 'jiminny.ai_activity_type';public const string AI_CALL_SCORING = 'jiminny.ai_call_scoring':3 usagespublic const string WEBHOOK_DELIVERY_SUCCESS = 'jiminny.webhook.delivery.success';4 usagespublic const string WEBHOOK_DELIVERY_FAILED = 'jiminny.webhook.delivery.failed';s usagespublic const string ACTIVITY_PROCESSING_STEP_TIME = 'jiminny.activity.processing.step.time';© ProviderRateLimiter.php= custom.logE laravel.logtiò accounts jiminny@localhost]4 SF [iminny@localhost]A console [STAGING] X& ho_local Uiminny@localnostCascade# console [Pkol)# console [eu)Hubspot Rate LimitingHubSpot Rate Limit H• Tx: Auto vSo jiminny020 A 18 X13A# id, vvid, type, provider, playbook_category_id, user_id, lead_id, contact_id, account_id, opportunity_# crm contiquration1d, crm provider 1d.trom activitles where crm_configuration_1d = 1 and type = "conterence"*and crm orovider 1d Is NO NULLIThought for 1s >Read AutomatedReportsCallbackService.phpThoughts >nl docker.desktop PERSONALQ Searchsupoont Dally • In sn 11m100% LzFri 8 May 11:49:25Datadog Metric Condr+0 ..latedReport->isAskJiminnyReport() we should send it toSign inloa method:Starting the Docker Engine...Docker Engine is the underlying technology that runs containersnew constant:11m1nny.ask 11m1nny_reports.usage';portsCallbackService.php:67-91 - Updated pushToDatadog() method to:p returns trueno report_type or media_type )led reportsGal .Enaine startinalRAMO00 GR CPUJ0.00% Dick•.. GR used (limit • •GR)_575SELECT * FROM automated_reports WHERE uvid_to_bin('18a06a75-afd2-476f-aadc-14d4057bdda2') = uuid;SELECT * FROM automated_report_results WHERE uuid_to_bin('582d4b50-8cd3-42a9-9819-d676ff8f3b43') = uuid;›_ © Update availableAsk anvthina (&4D+ @ CodelC° AdantiveW Windsurf Teamo42:25f 4 spaces...
|
NULL
|
-2899415107477444678
|
NULL
|
visual_change
|
ocr
|
NULL
|
Docker DesktopcaltProleteyD Datadogo clientonp© Co Docker DesktopcaltProleteyD Datadogo clientonp© Constants.php• _ Datelime_ Dealinsignts_DealKISKS• ElasticSearchEncodingEncrvptionC ES_ FeatureFlags0 FFMpeg• FileSystemN Gecko• GongD GuzzleHttp• KeyPointsiM Kiock• LanguageDetection• LiveFeedD LocksD Math• MediaPipelineMeetingBotMobileSettingsM ModalD Notification_ Nudae_ ParagraphBreaker_ ParticipantspeechPartitionedCookieaPlavistPropheta Prophet All/a ProsperWorksQueuev OJobC PateLimitAware.omt©RateLimitAwareWra@ RotsQueueConstante, 46© Constants.php(C) ProceccinaQueueConsRouterSam|D SCIMD SeederD SentryD Serializer©MatchActivityCrmData.php© CrmActivityService.phpO Automatedkeponkesultonp© Constants.phpx© CheckAndRetryRemoteMatch.phpRateLimitexception.php© SyncOpportunitiesJob.phpOpportunitysynclrait.ondImportBatchJobTrait.php(c)SyncHubspotObiects.phpC) HydrateCrmDataByExternalCallidJob.php© ConferenceCrmMatcherJob.php© MatchCrmData.php© Activity.php(C) DeraultUpdateCrmDataResolver.ohp© CachedCrmServiceDecorator.phpfinal class Constantsm| x8public const string CUSTOMER_API = 'jiminny.customer_api';lusdgeLusagesLusages1usageZ usagespublic const string ACTIVITY_LANGUAGE_DETECTION_SUCCESS = 'jiminny.activity.language_detection.success';public const string ACTIVITY_LANGUAGE_DETECTION_FAIL = 'jiminny.activity.language_detection.fail';public const string ASK_JIMINNY_DEAL_TEAM_REQUEST = 'jiminny.ask_jiminny_deals.team_write_requests';public const string ASK_JIMINNY_DEAL_TEAM_READ_REQUEST = 'jiminny.ask_jiminny_deals.team_read_requests';public const string CRM_AUTO_SYNC = 'jiminny.crm.auto-sync';public const string ACTIVITIES_DIALERS_STATS = 'jiminny.activities.dialers.stats';public const string ACTIVITIES_DIALERS_REASON_STATS = 'jiminny.activities.dialers.reason.stats';public const string ACTIVITIES_DIALERS_TRACK_IMPORTED = 'jiminny.activities.dialers.track.imported';public const string ACTIVITIES_QIALERS_TRACK_DURATION = 'jiminny.activities.dialers.track.duration';public const string AI_AUTOMATION_CRM_WRITE_EXECUTED = 'jiminny.ai_automation.crm_update_executed';public const string AUTOMATED_REPORTS = 'jiminny.automated_reports.usage';public const string ASK_LIMINNY_REPORTS = "jiminny.ask_jiminny_reports.usage';1 usagepublic const string HUBSPOT_WEBHOOK_SYNC = 'jiminny.crm.hubspot_webhook_sync';public const string AI_ACTIVITY_TYPE = 'jiminny.ai_activity_type';public const string AI_CALL_SCORING = 'jiminny.ai_call_scoring':3 usagespublic const string WEBHOOK_DELIVERY_SUCCESS = 'jiminny.webhook.delivery.success';4 usagespublic const string WEBHOOK_DELIVERY_FAILED = 'jiminny.webhook.delivery.failed';s usagespublic const string ACTIVITY_PROCESSING_STEP_TIME = 'jiminny.activity.processing.step.time';© ProviderRateLimiter.php= custom.logE laravel.logtiò accounts jiminny@localhost]4 SF [iminny@localhost]A console [STAGING] X& ho_local Uiminny@localnostCascade# console [Pkol)# console [eu)Hubspot Rate LimitingHubSpot Rate Limit H• Tx: Auto vSo jiminny020 A 18 X13A# id, vvid, type, provider, playbook_category_id, user_id, lead_id, contact_id, account_id, opportunity_# crm contiquration1d, crm provider 1d.trom activitles where crm_configuration_1d = 1 and type = "conterence"*and crm orovider 1d Is NO NULLIThought for 1s >Read AutomatedReportsCallbackService.phpThoughts >nl docker.desktop PERSONALQ Searchsupoont Dally • In sn 11m100% LzFri 8 May 11:49:25Datadog Metric Condr+0 ..latedReport->isAskJiminnyReport() we should send it toSign inloa method:Starting the Docker Engine...Docker Engine is the underlying technology that runs containersnew constant:11m1nny.ask 11m1nny_reports.usage';portsCallbackService.php:67-91 - Updated pushToDatadog() method to:p returns trueno report_type or media_type )led reportsGal .Enaine startinalRAMO00 GR CPUJ0.00% Dick•.. GR used (limit • •GR)_575SELECT * FROM automated_reports WHERE uvid_to_bin('18a06a75-afd2-476f-aadc-14d4057bdda2') = uuid;SELECT * FROM automated_report_results WHERE uuid_to_bin('582d4b50-8cd3-42a9-9819-d676ff8f3b43') = uuid;›_ © Update availableAsk anvthina (&4D+ @ CodelC° AdantiveW Windsurf Teamo42:25f 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6591
|
283
|
17
|
2026-05-08T06:45:07.744794+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778222707744_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window....
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Drag to record a part of the screen. Press ⌥W to select a window.","depth":1,"bounds":{"left":0.2986111,"top":0.48555556,"width":0.403125,"height":0.026666667},"on_screen":true,"role_description":"text"}]...
|
7027923345767264602
|
-7551080705130679904
|
app_switch
|
hybrid
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window.
FirefoxFileEditViewHistoryBookmarksProfiles→ToolsWindowHelp‹$0.lihlmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com§ Daily - Platform • now100% <•Fri 8 May 9:45:08 •Daily - Platformnow - 09:45-10:05C Join Google MeetDrag to record a part of the screen. Press TW to select a window.Nikolay NikolovSteliyan GeorgievLukas Kovalik9:45 AM | Daily - PlatformSộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6592
|
284
|
14
|
2026-05-08T06:45:07.869271+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778222707869_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window....
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Drag to record a part of the screen. Press ⌥W to select a window.","depth":1,"bounds":{"left":0.4035904,"top":0.4896249,"width":0.19298537,"height":0.01915403},"on_screen":true,"role_description":"text"}]...
|
7027923345767264602
|
-7551080705130679904
|
visual_change
|
hybrid
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window.
FV faVsco.jsTlolleaey(C) CrmAc|T SyncCrmFieldsTT SyncCrmMetadaT SystemStateTra(c) DataClient.onp* Responseexcepti© DecorateActivity.phBadkequest.ongy Locdlsearch.ong© LocalSearchInterfacC RemoteSearch.php) service.pnpy _ ListenersC) ConveriLeadActivilc) Purgelookupcache0 MetadataD Miarauioni07 Pipedrive• → OpportunitvSvncSstC) ApiFields.oho© Client.php© FieldDefinitions.php 117C) PioedriveAoiClient.() PioedriveAoiExceotl(C) Service.oholC) TokenStorade.ohoN Salesforcel• M Sieldel• M OnnortunitvMatcheD OpportunitySyncSt• M ProsnectSearchStr(c) Client.php© DecorateActivity.phu DeleteoblectsIraid© FieldDefinitions.phg)Pavloacbullder.ongC) Profile.phpC) @uervBuilder.php© QuervHandler.phdC) @uervlterator.php© QuervResults.phpC) Service.ohrM TraitsC BaseService.ohv(C) Activity.class CrmActivitvServiceprivate function updateParticipantsCrmData(Team SteamlActivity SactivitvCollection $participants,?ServiceInterface $crmService = null.): array {$matchedRecords = []:$matchedDomainRecords = []scnis->vaLluaceurmuontzouracionsaccivlcysth1s->decorator->settontiquration(sactiv1ty->geturmosth1s->decorator->seturmservice scrmservicetoreach (Sparticipants as Sparticipant "if (Sthis->shouldSkipParticipant(Sparticipant)) {if (! $this->shouldPerformLookup($participant, $team)) {Sthis->logger->info( CrmActivitvServicel Email domain belonas to the teai= Steam->aetidolSnarticinant->oetEma1Addresso- Plattorm Sprint 3 Q2 - Plattorm TeSevenShores|Hubspot|ExcepticXService-Desk - Queues - Platform• Jy 20807 check various issues wi•• Pull requests • jiminny/apr1L Useroilot 1 Ask liminny Report Gei• JY-20773 fix user pilot tracking ofi Problem loadina paqdD Search the CRM - HubSpot docs8 Jimini_ Now TablSrecords = $this->findCrmRecords($participant, $activity):if (! empty($records)) {SmatchedRecords[] = Srecords} else {srecoras = sch1s->t1ndurmuoma1nkecorasactivity: Sactivity.ssues APP-1EEDD: 31c8b6c919 hours ago JSONv Stack race DisplayyThere are 2 chained excentions in this event.08SevenShores\Hubspot\Exceptions\BadRequestClient error: 'POST https://api.hubapi.com/crm/v3/objects/contact/search (* resulted in a429 100 Many Kequests response:usatuswerormessaneotave neadied vou seconlyimttuenon уе авtсоте аотсвииaусіс виоаеiнCrashed in:/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24 in SevenShores Hubspot\Exceptions\HubspotException::create/app/Services/Crm/Hubspot/Client.php:94 in Jiminny|Services\Crm\Hubspot\ClientzgetPaginatedDataDraa to record a part of the screen. Press LW to select a window./app/Services/Crm/CrmActivityService.php:227 in Jiminny\Services\Crm\CrmActivityService::findCrmRecords223if (empty(Srecords) && Sparticipant->getName() !== null) ‹227Srecords = Sthis->decorator->matchByName(userto. sacoivity-›getusero->gecto.Obiect Jiminny Models Activity#37482263)Obiect Jiminny Modells Participant(#82989648)/app/Services/Crm/CrmActivityService.php:139 in Jiminny|Services\Crm\CrmActivityService:.updateParticipantsCrmDataJann/Hobs/Crm/MatchActivitvCrmData.nhn:107 inJiminnv|Johs|Crm|MatchActivitvCrmData«Jiminnv|Jobs\Crm\(closure)Jann/Johs/Crm/MatchActivitvCrmData.nhn-87 in Jiminnv|.Johs\Crm\ MatchActivitvCrmData-handleShow 1 more frameIn AppShow 2 more framesIn App• Copy as v00% 112• Ask Seer |*.Oithudei Jiralv Activity8 Assigned2 months agcby Lukas Kovalik to themselves.Marked as OngoingFirst Seen|o months agcv People(LK) particioatingSSIKMPIN viewedSimilar ssuesMeraed Issues...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22021
|
962
|
20
|
2026-05-12T06:45:36.749109+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778568336749_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window....
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Drag to record a part of the screen. Press ⌥W to select a window.","depth":1,"bounds":{"left":0.2986111,"top":0.48555556,"width":0.403125,"height":0.026666667},"on_screen":true,"role_description":"text"}]...
|
7027923345767264602
|
-7551080705130679904
|
app_switch
|
hybrid
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window.
FirefoxFileEdit→CViewHistoryBookmarksProfilesToolsWindowHelp‹$0lthlmeet.google.com/mie-gawc-dsi?authuser=lukas.kovalik%40jiminny.com1 Daily - Platform • now100% <478Tue 12 May 9:45:36 •=+Steliyan GeorgievDrag to record a part of the screen. Press TW to select a window.+Stefka StoyanovaLukas Kovalik9:45 AM | Daily - PlatformLộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22022
|
963
|
24
|
2026-05-12T06:45:36.875733+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778568336875_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window....
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Drag to record a part of the screen. Press ⌥W to select a window.","depth":1,"on_screen":true,"role_description":"text"}]...
|
7027923345767264602
|
-7551080705130679904
|
app_switch
|
hybrid
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window.
LateMoreiJiminny... v# curiosity lab# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# sofa-office# support# thank-yous# the_people_of_jimi…^? Direct messagesGalya Dimitrovaf Aneliva Angelovaf Petko KashinskiStefka Stoyanovaa Vasil VasilevNikolay Ivanov(3) Aneliva Angelova…rd, Sovan TanevVesLukas Kovali…—#:: Apps8 Toast1" Jira Cloud(m) Google Cale...€ BadKevwordsQuervExcer( ConfiqurationExceotion.o© CrmException.php-crmUndateSycention.ohn© DatabaseException.php& DealRisksEycention.nhn© DomainException.phpC EmailActivitvimnortEycen® ErrorDownloadingEmptyF) ErrorDownloadina Sile Syci© FileNotFoundException.plFileSystemException.php© Handler.php(& HttnRodDonuoctEycontinA UttnRodDGalya DimitrovaMessagesr FilesF UntitledLukas Kovalik 9:39 AM•може и за interest tracking да намправяедин тикeтGalya Dimitrova 9:39 AMи там ли не паботи.Lukas Kovalik 9:39 AMПетко ми писа че си пристига нещо мулипсваше така че трябва да видя каквода добавя в payloadне знам още какъв точно е проблемGalva Dimitrova 9:41 AMаха, ако можеш направо сега ла го.глелаш че порали различни пооблеми неработи пялата схема с нотификациите иПлануат от както сме пуснали фичіла Ивсеки ден след кой го клика за да давамрепорти на CS и много ми се иска даполкапаме автомативациятаLukas Kovalik = 9•41 AMnoбneGalva Dimitrova 9.42 AMто пьово ї ланхат имаха оъг и ги чаках.елна селмица ла го фикснатMessage Galva Dimitrova In a meeting •+ Aaervice lest.oneC RematchActivity(foller.phpC) Hubsoot/Service.onpMatchacuvityermData.phgSautomatedkeporc,••• 0 ¢→ CNew TabJy 20820 es reindex stream mode4 [JY-20725] [HubSpot] Optimise ClJY-20725 add HS rate limit har XPipelines - jiminny/apg( Pull requests • jiminny/app(JY-20773] User Pilot not receivin• Jy-20773 fix user pilot trackina ofi# [JY-20776) Automated report - seiTypeError: Leaque|Flysystem\FilesTypeError: Leaque|Flvsystem|FilesPlatform Sprint 3 Q2 - Platform Te— New Tal‹ $0 lil | Daily - Platform • now100% (4 Tue 12 May 9:45:36 •• github.comJY-20725 add HS rate limit handling on activities rematching #12066LakyLak wants to merge 4 commits into master from JY-20725-handle-HS-search-rate-limitMeasuresv U securty hotspotsv 94.8% Coverage on New Code0.0% Duplication on New CodeSee analysis details on SonarQube CloudLakyLak commented 14 hours agoaclaudeAuthor)...claude Bot commented 14 hours ago • edited -Claude finished @LakyLak's task in 2m 0s — View jobDraa to record a part of the screen. Press ZW to select a window.Coce ReviewV Gatherina context• Reading changed filesAnalyzing code qualityPosting review feedbackTheinre, vos ity, do if evilu feindbaek has been laraely adiresmnel) hevwore Mest of th. arlier sanin remsbelow.Still Open from Previous Review1. batchReadObiects() - 429s now silently converted to CrmExceptionThis is the original issue #1 but has gotten slightly worse. The try/catch in batchReadObiects() (line 316) catches all\Throwable and passes it to handleBatchError() (line 419), which unconditionally wraps it in CrmException :throw new CrmException("Failed to batch fetch {SobiectTvpe}: " . SerrorMessage) :Thic moane a 120 from tho hatch ADI ic.• Not detected as a rate-limit event• Never sets the Redis circuit-breaker• Thrown as CrmFycention instead of Ratel imitFycention...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
26949
|
1122
|
32
|
2026-05-12T13:59:59.001457+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778594399001_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window....
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Drag to record a part of the screen. Press ⌥W to select a window.","depth":1,"bounds":{"left":0.2986111,"top":0.48555556,"width":0.403125,"height":0.026666667},"on_screen":true,"role_description":"text"}]...
|
7027923345767264602
|
-7551080705130679904
|
app_switch
|
hybrid
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window.
FirefoxFile• 0Edit→ViewHistoryBookmarksProfilesCToolsWindowHelploblmeet.google.com/bdj-nvho-bms?authuser=lukas.kovalik%40jiminny.coma Retro - Platform • in 1 m100% <8Tue 12 May 16:59:58 •[EMAIL] accountLukas KovalikRetro - PlatformSteliyan Georgiev is in this callDrag to record a part of the screen. Press TW to select a window.'se Gemini to take notesare notes and transcriptStartJoin nowOther ways to join v9 LakyLak bos...4 System Defa...• FaceTime HD...[g Backgrounds...ccmeins it melting icapfiet s sing Aers enlini went ngeasi atreco diag or sfoze conetingadita. iTae emetraghost can turn it off. Learn moreLộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
26950
|
1123
|
40
|
2026-05-12T13:59:59.024395+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778594399024_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window....
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Drag to record a part of the screen. Press ⌥W to select a window.","depth":1,"on_screen":true,"role_description":"text"}]...
|
7027923345767264602
|
-7551080705130679904
|
app_switch
|
hybrid
|
NULL
|
Drag to record a part of the screen. Press ⌥W to s Drag to record a part of the screen. Press ⌥W to select a window.
• 0) TypeError: League|Flysystem|Fia CloudWatch | us-east-2(0) Pipelines - jiminny/ape4 [JY-20725) [HubSpot) Optimise C|JY-20725 add HS rate limit handl4(JY-207731 User Pilot not receivil(9) JY-20773 fix user pilot tracking fo| (JY-207761 Automated report - se(JY-20725 add HS rate limit handl-T (SRD-6793) Les Mills activity type*Platform Team - Backloo - Jiral1L Usernilot I Automated Renort ( XQ Search engagement, feedback, reports, users and moreDashboardsPeopleDataAnalyticeSessionsEngagemenFeedbackk EventsAutomated Report Generated ActiveseementAll users% Active users1/4603OverviewcompanyPageAll companiesAll pagesHL Active companies— 0%1/302—00e Total events occurren.• Unique uisers• Uiniaue comnanieslime periodLast 7 days4 Total events occurred— 0%A Avg. occurrences per user— 0%May 06. 2026Mav 07. 2026Drag to record a part of the screen. Press TW to select a window.-о-May 08. 2026-о-Mav 09. 2026May 10. 2026— Last 7 daysUser activity ~• Search.Userscto vscaiisio373771fd-b23c-410a-b5f7-a8c9db6cc71bTotal occurrencesFirst occurredMay 12, 2026 - 02:41 PM< Page 1 of 1 ›S 0 hul100% 52lue 1z MayD|w|мMay 11. 2026May 12. 2026Last occurredMay 12, 2026 - 02:47 PMDisplaving 1 - 1 of 1...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9049
|
406
|
25
|
2026-05-08T11:59:10.795690+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778241550795_m2.jpg...
|
Firefox
|
Vivacom - 5G мрежата с най-голямо покритие в Бълга Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет — Personal...
|
True
|
www.vivacom.bg/basket
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
EON
Pull requests · screenpipe/screenpipe · GitHub EON
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Премини към основното съдържание
Активиране на достъпност за хора със слабо зрение
Отворете менюто за достъпност
ЧАСТНИ КЛИЕНТИ
ЧАСТНИ КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
МАГАЗИНИ
МАГАЗИНИ
ГЛЕДАЙ EON
ГЛЕДАЙ EON
КОНТАКТИ
КОНТАКТИ
ОБЩИ УСЛОВИЯ
ОБЩИ УСЛОВИЯ
КАРТИ НА ПОКРИТИЕТО
КАРТИ НА ПОКРИТИЕТО
Vivacom Logo
Мобилни услуги
Мобилни услуги
Устройства
Устройства
EON
EON
EON планове
EON планове
EON пакети TV и интернет
EON пакети TV и интернет
EON SAT TV
EON SAT TV
EON TV
EON TV
EON TV без договор
EON TV без договор
Всичко за EON
Всичко за EON
Защо EON?
Защо EON?
EON Видеотека
EON Видеотека
EON Мобилно приложение
EON Мобилно приложение
EON Устройства
EON Устройства
Помощ
Помощ
Технологии
Технологии
EON SAT пакети
EON SAT пакети
EON 5G пакети
EON 5G пакети
Arena
Arena
Arena канали
Arena канали
Arena Select
Arena Select
Интернет
Интернет
Други услуги
Други услуги
Помощ
Помощ
Cart
Моята кошница
Моята кошница
Close
4G Mobix XL
4G Mobix XL
СА Mobix XL ¦ 10.54€ 20.62лв ¦ 5.75€ 11.25лв за 4М ¦ 11.50€ 22.49лв (5-24М)
24 месеца
Виж повече
Виж повече
5.75
€
|
11.25
лв.
/мес.
след 4 месеца
11.50
€
|
22.49
лв.
/мес.
Информация за поръчката
Информация за поръчката
Спестяваш
23.00
€
|
44.98
лв.
Общо сума на месец
4G Mobix XL
за първите 4 месеца
5.75
€
|
11.25
лв.
след 4 месеца
11.50
€
|
22.49
лв.
Общо плащане на месец
за първите 4 месеца
5.75...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"EON","depth":2,"bounds":{"left":0.8121675,"top":0.14764565,"width":0.0078125,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.26097074,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.27426863,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Home | Hostinger","depth":4,"bounds":{"left":0.26097074,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home | Hostinger","depth":5,"bounds":{"left":0.27426863,"top":0.09577015,"width":0.03025266,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login – Nginx Proxy Manager","depth":4,"bounds":{"left":0.26097074,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login – Nginx Proxy Manager","depth":5,"bounds":{"left":0.27426863,"top":0.12849163,"width":0.05069814,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.26097074,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.27426863,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.26097074,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.27426863,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.26097074,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.27426863,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.26097074,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.27426863,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.26097074,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.27426863,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет","depth":4,"bounds":{"left":0.26097074,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет","depth":5,"bounds":{"left":0.27426863,"top":0.32482043,"width":0.13696809,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.36236703,"top":0.32083002,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.26379654,"top":0.34796488,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.26379654,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.27476728,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.28590426,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.29704124,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.3081782,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Премини към основното съдържание","depth":7,"bounds":{"left":0.38730052,"top":0.0,"width":0.10472074,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Активиране на достъпност за хора със слабо зрение","depth":7,"bounds":{"left":0.38730052,"top":0.0,"width":0.10472074,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Отворете менюто за достъпност","depth":7,"bounds":{"left":0.38730052,"top":0.0,"width":0.10472074,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"ЧАСТНИ КЛИЕНТИ","depth":10,"bounds":{"left":0.41539228,"top":0.0518755,"width":0.0631649,"height":0.037110932},"on_screen":true,"help_text":"Частни клиенти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ЧАСТНИ КЛИЕНТИ","depth":11,"bounds":{"left":0.42337102,"top":0.061452515,"width":0.04720745,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"БИЗНЕС КЛИЕНТИ","depth":10,"bounds":{"left":0.47855717,"top":0.0518755,"width":0.062832445,"height":0.037110932},"on_screen":true,"help_text":"Бизнес клиенти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"БИЗНЕС КЛИЕНТИ","depth":11,"bounds":{"left":0.4865359,"top":0.061452515,"width":0.046875,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"МАГАЗИНИ","depth":10,"bounds":{"left":0.67985374,"top":0.0518755,"width":0.04488032,"height":0.037110932},"on_screen":true,"help_text":"Магазини","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"МАГАЗИНИ","depth":11,"bounds":{"left":0.6878325,"top":0.061452515,"width":0.028922873,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ГЛЕДАЙ EON","depth":10,"bounds":{"left":0.72473407,"top":0.0518755,"width":0.04837101,"height":0.037110932},"on_screen":true,"help_text":"Гледай EON","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ГЛЕДАЙ EON","depth":11,"bounds":{"left":0.73271275,"top":0.061452515,"width":0.032413565,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"КОНТАКТИ","depth":10,"bounds":{"left":0.773105,"top":0.0518755,"width":0.044049203,"height":0.037110932},"on_screen":true,"help_text":"Контакти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"КОНТАКТИ","depth":11,"bounds":{"left":0.78108376,"top":0.061452515,"width":0.028091755,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ОБЩИ УСЛОВИЯ","depth":10,"bounds":{"left":0.8171542,"top":0.0518755,"width":0.058011968,"height":0.037110932},"on_screen":true,"help_text":"Общи условия","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ОБЩИ УСЛОВИЯ","depth":11,"bounds":{"left":0.82513297,"top":0.061452515,"width":0.042054523,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"КАРТИ НА ПОКРИТИЕТО","depth":10,"bounds":{"left":0.87516624,"top":0.0518755,"width":0.07829122,"height":0.037110932},"on_screen":true,"help_text":"Карти на покритието","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"КАРТИ НА ПОКРИТИЕТО","depth":11,"bounds":{"left":0.883145,"top":0.061452515,"width":0.062333778,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Vivacom Logo","depth":8,"bounds":{"left":0.41539228,"top":0.1217079,"width":0.08643617,"height":0.0207502},"on_screen":true,"help_text":"Home","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Мобилни услуги","depth":10,"bounds":{"left":0.67869014,"top":0.121308856,"width":0.053523935,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Мобилни услуги","depth":11,"bounds":{"left":0.67869014,"top":0.12090982,"width":0.053523935,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Устройства","depth":10,"bounds":{"left":0.74551195,"top":0.121308856,"width":0.043218084,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Устройства","depth":11,"bounds":{"left":0.74551195,"top":0.12090982,"width":0.043218084,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON","depth":10,"bounds":{"left":0.80202794,"top":0.121308856,"width":0.013464096,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"EON","depth":11,"bounds":{"left":0.80202794,"top":0.12090982,"width":0.013464096,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"EON планове","depth":10,"bounds":{"left":0.61835104,"top":0.21548285,"width":0.08610372,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"EON планове","depth":11,"bounds":{"left":0.61835104,"top":0.2150838,"width":0.041722074,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON пакети TV и интернет","depth":12,"bounds":{"left":0.61835104,"top":0.24501197,"width":0.07130984,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON пакети TV и интернет","depth":13,"bounds":{"left":0.61835104,"top":0.24461293,"width":0.07130984,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON SAT TV","depth":12,"bounds":{"left":0.61835104,"top":0.26855546,"width":0.02825798,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON SAT TV","depth":13,"bounds":{"left":0.61835104,"top":0.26815644,"width":0.02825798,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON TV","depth":12,"bounds":{"left":0.61835104,"top":0.29209897,"width":0.017785905,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON TV","depth":13,"bounds":{"left":0.61835104,"top":0.29169992,"width":0.017785905,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON TV без договор","depth":12,"bounds":{"left":0.61835104,"top":0.31564245,"width":0.05086436,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON TV без договор","depth":13,"bounds":{"left":0.61835104,"top":0.31524342,"width":0.05086436,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Всичко за EON","depth":10,"bounds":{"left":0.7094415,"top":0.21548285,"width":0.08610372,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Всичко за EON","depth":11,"bounds":{"left":0.7094415,"top":0.2150838,"width":0.046875,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Защо EON?","depth":12,"bounds":{"left":0.7094415,"top":0.24501197,"width":0.028091755,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Защо EON?","depth":13,"bounds":{"left":0.7094415,"top":0.24461293,"width":0.028091755,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON Видеотека","depth":12,"bounds":{"left":0.7094415,"top":0.26855546,"width":0.040392287,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON Видеотека","depth":13,"bounds":{"left":0.7094415,"top":0.26815644,"width":0.040392287,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON Мобилно приложение","depth":12,"bounds":{"left":0.7094415,"top":0.29209897,"width":0.06665558,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON Мобилно приложение","depth":13,"bounds":{"left":0.7094415,"top":0.29169992,"width":0.06665558,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON Устройства","depth":12,"bounds":{"left":0.7094415,"top":0.31564245,"width":0.044714097,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON Устройства","depth":13,"bounds":{"left":0.7094415,"top":0.31524342,"width":0.044714097,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Помощ","depth":12,"bounds":{"left":0.7094415,"top":0.33918595,"width":0.018284574,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Помощ","depth":13,"bounds":{"left":0.7094415,"top":0.3387869,"width":0.018284574,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Технологии","depth":10,"bounds":{"left":0.8005319,"top":0.21548285,"width":0.08610372,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Технологии","depth":11,"bounds":{"left":0.8005319,"top":0.2150838,"width":0.037898935,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON SAT пакети","depth":12,"bounds":{"left":0.8005319,"top":0.24501197,"width":0.041223403,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON SAT пакети","depth":13,"bounds":{"left":0.8005319,"top":0.24461293,"width":0.041223403,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON 5G пакети","depth":12,"bounds":{"left":0.8005319,"top":0.26855546,"width":0.03873005,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON 5G пакети","depth":13,"bounds":{"left":0.8005319,"top":0.26815644,"width":0.03873005,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Arena","depth":10,"bounds":{"left":0.89162236,"top":0.21548285,"width":0.08610372,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Arena","depth":11,"bounds":{"left":0.89162236,"top":0.2150838,"width":0.018450798,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Arena канали","depth":12,"bounds":{"left":0.89162236,"top":0.24501197,"width":0.032912236,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Arena канали","depth":13,"bounds":{"left":0.89162236,"top":0.24461293,"width":0.032912236,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Arena Select","depth":12,"bounds":{"left":0.89162236,"top":0.26855546,"width":0.03125,"height":0.01715882},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Arena Select","depth":13,"bounds":{"left":0.89162236,"top":0.26815644,"width":0.03125,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Интернет","depth":10,"bounds":{"left":0.8287899,"top":0.121308856,"width":0.03673537,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Интернет","depth":11,"bounds":{"left":0.8287899,"top":0.12090982,"width":0.03673537,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Други услуги","depth":10,"bounds":{"left":0.87882316,"top":0.121308856,"width":0.044215426,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Други услуги","depth":11,"bounds":{"left":0.87882316,"top":0.12090982,"width":0.044215426,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Помощ","depth":10,"bounds":{"left":0.93633646,"top":0.121308856,"width":0.023769947,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Помощ","depth":11,"bounds":{"left":0.93633646,"top":0.12090982,"width":0.023769947,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Cart","depth":8,"bounds":{"left":0.9734042,"top":0.088986434,"width":0.026595745,"height":0.08619314},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Моята кошница","depth":9,"bounds":{"left":0.52377,"top":0.17478053,"width":0.32712767,"height":0.056664005},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Моята кошница","depth":10,"bounds":{"left":0.65442157,"top":0.20590582,"width":0.06582447,"height":0.026735835},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Close","depth":9,"bounds":{"left":0.70428854,"top":0.2601756,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"4G Mobix XL","depth":10,"bounds":{"left":0.5304189,"top":0.27294493,"width":0.07579787,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4G Mobix XL","depth":11,"bounds":{"left":0.5304189,"top":0.2725459,"width":0.039228722,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"СА Mobix XL ¦ 10.54€ 20.62лв ¦ 5.75€ 11.25лв за 4М ¦ 11.50€ 22.49лв (5-24М)","depth":12,"bounds":{"left":0.5304189,"top":0.29968077,"width":0.08194814,"height":0.030327214},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"24 месеца","depth":11,"bounds":{"left":0.5304189,"top":0.33838788,"width":0.020611702,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Виж повече","depth":10,"bounds":{"left":0.5304189,"top":0.35315242,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Виж повече","depth":11,"bounds":{"left":0.5304189,"top":0.3527534,"width":0.026928192,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.75","depth":10,"bounds":{"left":0.66107047,"top":0.2725459,"width":0.009973404,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"bounds":{"left":0.6710439,"top":0.2725459,"width":0.0043218085,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"bounds":{"left":0.6753657,"top":0.2725459,"width":0.0011635638,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.25","depth":10,"bounds":{"left":0.67736036,"top":0.2725459,"width":0.012466756,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"bounds":{"left":0.68982714,"top":0.2725459,"width":0.007480053,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/мес.","depth":10,"bounds":{"left":0.69730717,"top":0.27613726,"width":0.009640957,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"след 4 месеца","depth":10,"bounds":{"left":0.6359708,"top":0.28930566,"width":0.030086435,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.50","depth":10,"bounds":{"left":0.66605717,"top":0.28930566,"width":0.009142287,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"bounds":{"left":0.67519945,"top":0.28930566,"width":0.003656915,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"bounds":{"left":0.6788564,"top":0.28930566,"width":0.0009973404,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22.49","depth":10,"bounds":{"left":0.6805186,"top":0.28930566,"width":0.011968086,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"bounds":{"left":0.6924867,"top":0.28930566,"width":0.005485372,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/мес.","depth":10,"bounds":{"left":0.69797206,"top":0.29090184,"width":0.008976064,"height":0.013567438},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Информация за поръчката","depth":9,"bounds":{"left":0.74052525,"top":0.27294493,"width":0.10372341,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Информация за поръчката","depth":10,"bounds":{"left":0.74667555,"top":0.2725459,"width":0.09142287,"height":0.021947326},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Спестяваш","depth":10,"bounds":{"left":0.7578125,"top":0.32521948,"width":0.032912236,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"23.00","depth":10,"bounds":{"left":0.7960439,"top":0.32521948,"width":0.013962766,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"bounds":{"left":0.8100067,"top":0.32521948,"width":0.004155585,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"bounds":{"left":0.81416225,"top":0.32521948,"width":0.0011635638,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44.98","depth":10,"bounds":{"left":0.8161569,"top":0.32521948,"width":0.014295213,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"bounds":{"left":0.83045214,"top":0.32521948,"width":0.0071476065,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Общо сума на месец","depth":10,"bounds":{"left":0.7471742,"top":0.38946527,"width":0.055352394,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4G Mobix XL","depth":10,"bounds":{"left":0.7471742,"top":0.41580206,"width":0.029421542,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"за първите 4 месеца","depth":10,"bounds":{"left":0.7524933,"top":0.43375897,"width":0.047872342,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.75","depth":10,"bounds":{"left":0.80452126,"top":0.43256184,"width":0.00930851,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"bounds":{"left":0.8138298,"top":0.43256184,"width":0.004155585,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"bounds":{"left":0.81798536,"top":0.43256184,"width":0.0011635638,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.25","depth":10,"bounds":{"left":0.81998,"top":0.43256184,"width":0.011136968,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"bounds":{"left":0.83111703,"top":0.43256184,"width":0.006482713,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"след 4 месеца","depth":10,"bounds":{"left":0.7524933,"top":0.4501197,"width":0.03174867,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.50","depth":10,"bounds":{"left":0.80086434,"top":0.44892257,"width":0.010638298,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"bounds":{"left":0.81150264,"top":0.44892257,"width":0.004155585,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"bounds":{"left":0.8156583,"top":0.44892257,"width":0.0009973404,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22.49","depth":10,"bounds":{"left":0.81765294,"top":0.44892257,"width":0.013464096,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"bounds":{"left":0.83111703,"top":0.44892257,"width":0.006482713,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Общо плащане на месец","depth":10,"bounds":{"left":0.7471742,"top":0.48802873,"width":0.06632314,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"за първите 4 месеца","depth":10,"bounds":{"left":0.7471742,"top":0.50558656,"width":0.05069814,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.75","depth":10,"bounds":{"left":0.80136305,"top":0.50438946,"width":0.010139627,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-432278551325731891
|
7382950435744743987
|
visual_change
|
accessibility
|
NULL
|
EON
Pull requests · screenpipe/screenpipe · GitHub EON
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Премини към основното съдържание
Активиране на достъпност за хора със слабо зрение
Отворете менюто за достъпност
ЧАСТНИ КЛИЕНТИ
ЧАСТНИ КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
МАГАЗИНИ
МАГАЗИНИ
ГЛЕДАЙ EON
ГЛЕДАЙ EON
КОНТАКТИ
КОНТАКТИ
ОБЩИ УСЛОВИЯ
ОБЩИ УСЛОВИЯ
КАРТИ НА ПОКРИТИЕТО
КАРТИ НА ПОКРИТИЕТО
Vivacom Logo
Мобилни услуги
Мобилни услуги
Устройства
Устройства
EON
EON
EON планове
EON планове
EON пакети TV и интернет
EON пакети TV и интернет
EON SAT TV
EON SAT TV
EON TV
EON TV
EON TV без договор
EON TV без договор
Всичко за EON
Всичко за EON
Защо EON?
Защо EON?
EON Видеотека
EON Видеотека
EON Мобилно приложение
EON Мобилно приложение
EON Устройства
EON Устройства
Помощ
Помощ
Технологии
Технологии
EON SAT пакети
EON SAT пакети
EON 5G пакети
EON 5G пакети
Arena
Arena
Arena канали
Arena канали
Arena Select
Arena Select
Интернет
Интернет
Други услуги
Други услуги
Помощ
Помощ
Cart
Моята кошница
Моята кошница
Close
4G Mobix XL
4G Mobix XL
СА Mobix XL ¦ 10.54€ 20.62лв ¦ 5.75€ 11.25лв за 4М ¦ 11.50€ 22.49лв (5-24М)
24 месеца
Виж повече
Виж повече
5.75
€
|
11.25
лв.
/мес.
след 4 месеца
11.50
€
|
22.49
лв.
/мес.
Информация за поръчката
Информация за поръчката
Спестяваш
23.00
€
|
44.98
лв.
Общо сума на месец
4G Mobix XL
за първите 4 месеца
5.75
€
|
11.25
лв.
след 4 месеца
11.50
€
|
22.49
лв.
Общо плащане на месец
за първите 4 месеца
5.75...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9048
|
405
|
19
|
2026-05-08T11:59:10.778363+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778241550778_m1.jpg...
|
Firefox
|
Vivacom - 5G мрежата с най-голямо покритие в Бълга Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет — Personal...
|
True
|
www.vivacom.bg/basket
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
EON
Pull requests · screenpipe/screenpipe · GitHub EON
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Премини към основното съдържание
Активиране на достъпност за хора със слабо зрение
Отворете менюто за достъпност
ЧАСТНИ КЛИЕНТИ
ЧАСТНИ КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
МАГАЗИНИ
МАГАЗИНИ
ГЛЕДАЙ EON
ГЛЕДАЙ EON
КОНТАКТИ
КОНТАКТИ
ОБЩИ УСЛОВИЯ
ОБЩИ УСЛОВИЯ
КАРТИ НА ПОКРИТИЕТО
КАРТИ НА ПОКРИТИЕТО
Vivacom Logo
Мобилни услуги
Мобилни услуги
Устройства
Устройства
EON
EON
EON планове
EON планове
EON пакети TV и интернет
EON пакети TV и интернет
EON SAT TV
EON SAT TV
EON TV
EON TV
EON TV без договор
EON TV без договор
Всичко за EON
Всичко за EON
Защо EON?
Защо EON?
EON Видеотека
EON Видеотека
EON Мобилно приложение
EON Мобилно приложение
EON Устройства
EON Устройства
Помощ
Помощ
Технологии
Технологии
EON SAT пакети
EON SAT пакети
EON 5G пакети
EON 5G пакети
Arena
Arena
Arena канали
Arena канали
Arena Select
Arena Select
Интернет
Интернет
Други услуги
Други услуги
Помощ
Помощ
Cart
Моята кошница
Моята кошница
Close
4G Mobix XL
4G Mobix XL
СА Mobix XL ¦ 10.54€ 20.62лв ¦ 5.75€ 11.25лв за 4М ¦ 11.50€ 22.49лв (5-24М)
24 месеца
Виж повече
Виж повече
5.75
€
|
11.25
лв.
/мес.
след 4 месеца
11.50
€
|
22.49
лв.
/мес.
Информация за поръчката
Информация за поръчката
Спестяваш
23.00
€
|
44.98
лв.
Общо сума на месец
4G Mobix XL
за първите 4 месеца
5.75
€
|
11.25
лв.
след 4 месеца
11.50
€
|
22.49
лв.
Общо плащане на месец
за първите 4 месеца
5.75
€
|
11.25
лв.
след 4 месеца
11.50
€
|
22.49
лв.
Плащаш сега
Общо
0.00
€
|
0.00
лв.
Поръчай Поръчай
Поръчай
Поръчай
Имаш клиентски профил? Имаш клиентски профил?
Имаш клиентски профил?
Имаш клиентски профил?
Компанията
Компанията
За нас
За нас
Етика и съответствие
Етика и съответствие
Марката Vivacom
Марката Vivacom
Мениджмънт
Мениджмънт
Социална отговорност
Социална отговорност
Новини
Новини
Кариери
Кариери
Доставчици
Доставчици
Доклад за устойчиво развитие
Доклад за устойчиво развитие
Частни клиенти
Частни клиенти
Мобилни планове
Мобилни планове
Мобилен интернет
Мобилен интернет
Устройства
Устройства
Интернет пакети
Интернет пакети
Програма Лоялен клиент
Програма Лоялен клиент
Правила и условия
Правила и условия
Общи условия
Общи условия
Мобилно покритие
Мобилно покритие
Лични данни
Лични данни
Правила за ползване
Правила за ползване
Роуминг
Роуминг
Политика за бисквитките
Политика за бисквитките
Полезни връзки
Полезни връзки
Устройство в сервиз
Устройство в сервиз
Спешни номера
Спешни номера
Активиране на EON TV
Активиране на EON TV
Настройки на CA модул
Настройки на CA модул
Застраховки
Застраховки
Планове за хора с увреждания
Планове за хора с увреждания
Достъпност на сайта
Достъпност на сайта
Електронни фактури
Електронни фактури
EUR BGN
Валутен курс: 1 EUR = 1.95583 лв.
© VIVACOM 2026
google app - целевият уебсайт може да не е наличен
App store - отвори в нов раздел
Huawei store - отвори в нов раздел
Facebook
TikTok
YouTube
Instagram
Linkedin
United group - отвори в нов раздел
Отворена джаджа за чат
1
Open CMP widget
javascript:void(0)...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"EON","depth":2,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Home | Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home | Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login – Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login – Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.009375,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.03263889,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.05590278,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.079166666,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Премини към основното съдържание","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Активиране на достъпност за хора със слабо зрение","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Отворете менюто за достъпност","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"ЧАСТНИ КЛИЕНТИ","depth":10,"on_screen":true,"help_text":"Частни клиенти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ЧАСТНИ КЛИЕНТИ","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"БИЗНЕС КЛИЕНТИ","depth":10,"on_screen":true,"help_text":"Бизнес клиенти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"БИЗНЕС КЛИЕНТИ","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"МАГАЗИНИ","depth":10,"on_screen":true,"help_text":"Магазини","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"МАГАЗИНИ","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ГЛЕДАЙ EON","depth":10,"on_screen":true,"help_text":"Гледай EON","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ГЛЕДАЙ EON","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"КОНТАКТИ","depth":10,"on_screen":true,"help_text":"Контакти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"КОНТАКТИ","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ОБЩИ УСЛОВИЯ","depth":10,"on_screen":true,"help_text":"Общи условия","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ОБЩИ УСЛОВИЯ","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"КАРТИ НА ПОКРИТИЕТО","depth":10,"on_screen":true,"help_text":"Карти на покритието","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"КАРТИ НА ПОКРИТИЕТО","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Vivacom Logo","depth":8,"on_screen":true,"help_text":"Home","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Мобилни услуги","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Мобилни услуги","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Устройства","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Устройства","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"EON","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"EON планове","depth":10,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"EON планове","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON пакети TV и интернет","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON пакети TV и интернет","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON SAT TV","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON SAT TV","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON TV","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON TV","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON TV без договор","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON TV без договор","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Всичко за EON","depth":10,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Всичко за EON","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Защо EON?","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Защо EON?","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON Видеотека","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON Видеотека","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON Мобилно приложение","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON Мобилно приложение","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON Устройства","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON Устройства","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Помощ","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Помощ","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Технологии","depth":10,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Технологии","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON SAT пакети","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON SAT пакети","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"EON 5G пакети","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EON 5G пакети","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Arena","depth":10,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Arena","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Arena канали","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Arena канали","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Arena Select","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Arena Select","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Интернет","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Интернет","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Други услуги","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Други услуги","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Помощ","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Помощ","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Cart","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Моята кошница","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Моята кошница","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Close","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"4G Mobix XL","depth":10,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4G Mobix XL","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"СА Mobix XL ¦ 10.54€ 20.62лв ¦ 5.75€ 11.25лв за 4М ¦ 11.50€ 22.49лв (5-24М)","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"24 месеца","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Виж повече","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Виж повече","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.75","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.25","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/мес.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"след 4 месеца","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.50","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22.49","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/мес.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Информация за поръчката","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Информация за поръчката","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Спестяваш","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"23.00","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44.98","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Общо сума на месец","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4G Mobix XL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"за първите 4 месеца","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.75","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.25","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"след 4 месеца","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.50","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22.49","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Общо плащане на месец","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"за първите 4 месеца","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.75","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.25","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"след 4 месеца","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11.50","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22.49","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Плащаш сега","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Общо","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"0.00","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"€","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"|","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"0.00","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"лв.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Поръчай Поръчай","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Поръчай","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Поръчай","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Имаш клиентски профил? Имаш клиентски профил?","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Имаш клиентски профил?","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Имаш клиентски профил?","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Компанията","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Компанията","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"За нас","depth":10,"bounds":{"left":0.515625,"top":0.0,"width":0.029513888,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"За нас","depth":11,"bounds":{"left":0.515625,"top":0.0,"width":0.029513888,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Етика и съответствие","depth":10,"bounds":{"left":0.515625,"top":0.0,"width":0.11423611,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Етика и съответствие","depth":11,"bounds":{"left":0.515625,"top":0.0,"width":0.11423611,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Марката Vivacom","depth":10,"bounds":{"left":0.515625,"top":0.0,"width":0.08229167,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Марката Vivacom","depth":11,"bounds":{"left":0.515625,"top":0.0,"width":0.08229167,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Мениджмънт","depth":10,"bounds":{"left":0.515625,"top":0.006666667,"width":0.06388889,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Мениджмънт","depth":11,"bounds":{"left":0.515625,"top":0.006666667,"width":0.06388889,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Социална отговорност","depth":10,"bounds":{"left":0.515625,"top":0.03888889,"width":0.114583336,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Социална отговорност","depth":11,"bounds":{"left":0.515625,"top":0.038333334,"width":0.114583336,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Новини","depth":10,"bounds":{"left":0.515625,"top":0.07111111,"width":0.035069443,"height":0.020555556},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Новини","depth":11,"bounds":{"left":0.515625,"top":0.07055555,"width":0.035069443,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Кариери","depth":10,"bounds":{"left":0.515625,"top":0.10277778,"width":0.039930556,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Кариери","depth":11,"bounds":{"left":0.515625,"top":0.10277778,"width":0.039930556,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Доставчици","depth":10,"bounds":{"left":0.515625,"top":0.135,"width":0.060069446,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Доставчици","depth":11,"bounds":{"left":0.515625,"top":0.13444445,"width":0.060069446,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Доклад за устойчиво развитие","depth":10,"bounds":{"left":0.515625,"top":0.16722222,"width":0.15104167,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Доклад за устойчиво развитие","depth":11,"bounds":{"left":0.515625,"top":0.16666667,"width":0.15104167,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Частни клиенти","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Частни клиенти","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Мобилни планове","depth":10,"bounds":{"left":0.69340277,"top":0.0,"width":0.08229167,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Мобилни планове","depth":11,"bounds":{"left":0.69340277,"top":0.0,"width":0.08229167,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Мобилен интернет","depth":10,"bounds":{"left":0.69340277,"top":0.0,"width":0.094444446,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Мобилен интернет","depth":11,"bounds":{"left":0.69340277,"top":0.0,"width":0.094444446,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Устройства","depth":10,"bounds":{"left":0.69340277,"top":0.0,"width":0.060763888,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Устройства","depth":11,"bounds":{"left":0.69340277,"top":0.0,"width":0.060763888,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Интернет пакети","depth":10,"bounds":{"left":0.69340277,"top":0.006666667,"width":0.08888889,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Интернет пакети","depth":11,"bounds":{"left":0.69340277,"top":0.006666667,"width":0.08888889,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Програма Лоялен клиент","depth":10,"bounds":{"left":0.69340277,"top":0.03888889,"width":0.119097225,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Програма Лоялен клиент","depth":11,"bounds":{"left":0.69340277,"top":0.038333334,"width":0.119097225,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Правила и условия","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Правила и условия","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Общи условия","depth":10,"bounds":{"left":0.87118053,"top":0.0,"width":0.06631944,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Общи условия","depth":11,"bounds":{"left":0.87118053,"top":0.0,"width":0.06631944,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Мобилно покритие","depth":10,"bounds":{"left":0.87118053,"top":0.0,"width":0.09131944,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Мобилно покритие","depth":11,"bounds":{"left":0.87118053,"top":0.0,"width":0.09131944,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Лични данни","depth":10,"bounds":{"left":0.87118053,"top":0.0,"width":0.058680557,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Лични данни","depth":11,"bounds":{"left":0.87118053,"top":0.0,"width":0.058680557,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Правила за ползване","depth":10,"bounds":{"left":0.87118053,"top":0.006666667,"width":0.098958336,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Правила за ползване","depth":11,"bounds":{"left":0.87118053,"top":0.006666667,"width":0.098958336,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Роуминг","depth":10,"bounds":{"left":0.87118053,"top":0.03888889,"width":0.039930556,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Роуминг","depth":11,"bounds":{"left":0.87118053,"top":0.038333334,"width":0.039930556,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Политика за бисквитките","depth":10,"bounds":{"left":0.87118053,"top":0.07111111,"width":0.128125,"height":0.020555556},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Политика за бисквитките","depth":11,"bounds":{"left":0.87118053,"top":0.07055555,"width":0.128125,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Полезни връзки","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Полезни връзки","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Устройство в сервиз","depth":10,"bounds":{"left":1.0,"top":0.0,"width":-0.0489583,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Устройство в сервиз","depth":11,"bounds":{"left":1.0,"top":0.0,"width":-0.0489583,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Спешни номера","depth":10,"bounds":{"left":1.0,"top":0.0,"width":-0.0489583,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Спешни номера","depth":11,"bounds":{"left":1.0,"top":0.0,"width":-0.0489583,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Активиране на EON TV","depth":10,"bounds":{"left":1.0,"top":0.0,"width":-0.0489583,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Активиране на EON TV","depth":11,"bounds":{"left":1.0,"top":0.0,"width":-0.0489583,"height":0.021666666},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Настройки на CA модул","depth":10,"bounds":{"left":1.0,"top":0.006666667,"width":-0.0489583,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Настройки на CA модул","depth":11,"bounds":{"left":1.0,"top":0.006666667,"width":-0.0489583,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Застраховки","depth":10,"bounds":{"left":1.0,"top":0.03888889,"width":-0.0489583,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Застраховки","depth":11,"bounds":{"left":1.0,"top":0.038333334,"width":-0.0489583,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Планове за хора с увреждания","depth":10,"bounds":{"left":1.0,"top":0.07111111,"width":-0.0489583,"height":0.020555556},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Планове за хора с увреждания","depth":11,"bounds":{"left":1.0,"top":0.07055555,"width":-0.0489583,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Достъпност на сайта","depth":10,"bounds":{"left":1.0,"top":0.10277778,"width":-0.0489583,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Достъпност на сайта","depth":11,"bounds":{"left":1.0,"top":0.10277778,"width":-0.0489583,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Електронни фактури","depth":10,"bounds":{"left":1.0,"top":0.135,"width":-0.0489583,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронни фактури","depth":11,"bounds":{"left":1.0,"top":0.13444445,"width":-0.0489583,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"EUR BGN","depth":9,"bounds":{"left":0.7982639,"top":0.26166666,"width":0.035416666,"height":0.018888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Валутен курс: 1 EUR = 1.95583 лв.","depth":9,"bounds":{"left":0.7920139,"top":0.29055557,"width":0.15833333,"height":0.021666666},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"© VIVACOM 2026","depth":9,"bounds":{"left":0.41284722,"top":0.3961111,"width":0.08715278,"height":0.025},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"google app - целевият уебсайт може да не е наличен","depth":8,"bounds":{"left":0.5364583,"top":0.38666666,"width":0.09375,"height":0.044444446},"on_screen":false,"help_text":"Google app","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"App store - отвори в нов раздел","depth":8,"bounds":{"left":0.640625,"top":0.38666666,"width":0.09375,"height":0.044444446},"on_screen":false,"help_text":"App store","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Huawei store - отвори в нов раздел","depth":8,"bounds":{"left":0.7447917,"top":0.38666666,"width":0.09375,"height":0.044444446},"on_screen":false,"help_text":"Huawei store","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Facebook","depth":8,"bounds":{"left":0.92118055,"top":0.3861111,"width":0.028472222,"height":0.045555554},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"TikTok","depth":8,"bounds":{"left":0.9635417,"top":0.3861111,"width":0.028472222,"height":0.045555554},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"YouTube","depth":8,"bounds":{"left":1.0,"top":0.3861111,"width":-0.005902767,"height":0.045555554},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Instagram","depth":8,"bounds":{"left":1.0,"top":0.3861111,"width":-0.048263907,"height":0.045555554},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Linkedin","depth":8,"bounds":{"left":1.0,"top":0.3861111,"width":-0.09062505,"height":0.045555554},"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"United group - отвори в нов раздел","depth":8,"on_screen":false,"help_text":"United group","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Отворена джаджа за чат","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open CMP widget","depth":7,"bounds":{"left":0.225,"top":0.0,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"javascript:void(0)","depth":5,"bounds":{"left":0.22083333,"top":0.0,"width":0.063541666,"height":0.015},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
8618671153284067756
|
7738735407602439779
|
click
|
accessibility
|
NULL
|
EON
Pull requests · screenpipe/screenpipe · GitHub EON
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Vivacom - 5G мрежата с най-голямо покритие в България | EON и интернет
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Премини към основното съдържание
Активиране на достъпност за хора със слабо зрение
Отворете менюто за достъпност
ЧАСТНИ КЛИЕНТИ
ЧАСТНИ КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
МАГАЗИНИ
МАГАЗИНИ
ГЛЕДАЙ EON
ГЛЕДАЙ EON
КОНТАКТИ
КОНТАКТИ
ОБЩИ УСЛОВИЯ
ОБЩИ УСЛОВИЯ
КАРТИ НА ПОКРИТИЕТО
КАРТИ НА ПОКРИТИЕТО
Vivacom Logo
Мобилни услуги
Мобилни услуги
Устройства
Устройства
EON
EON
EON планове
EON планове
EON пакети TV и интернет
EON пакети TV и интернет
EON SAT TV
EON SAT TV
EON TV
EON TV
EON TV без договор
EON TV без договор
Всичко за EON
Всичко за EON
Защо EON?
Защо EON?
EON Видеотека
EON Видеотека
EON Мобилно приложение
EON Мобилно приложение
EON Устройства
EON Устройства
Помощ
Помощ
Технологии
Технологии
EON SAT пакети
EON SAT пакети
EON 5G пакети
EON 5G пакети
Arena
Arena
Arena канали
Arena канали
Arena Select
Arena Select
Интернет
Интернет
Други услуги
Други услуги
Помощ
Помощ
Cart
Моята кошница
Моята кошница
Close
4G Mobix XL
4G Mobix XL
СА Mobix XL ¦ 10.54€ 20.62лв ¦ 5.75€ 11.25лв за 4М ¦ 11.50€ 22.49лв (5-24М)
24 месеца
Виж повече
Виж повече
5.75
€
|
11.25
лв.
/мес.
след 4 месеца
11.50
€
|
22.49
лв.
/мес.
Информация за поръчката
Информация за поръчката
Спестяваш
23.00
€
|
44.98
лв.
Общо сума на месец
4G Mobix XL
за първите 4 месеца
5.75
€
|
11.25
лв.
след 4 месеца
11.50
€
|
22.49
лв.
Общо плащане на месец
за първите 4 месеца
5.75
€
|
11.25
лв.
след 4 месеца
11.50
€
|
22.49
лв.
Плащаш сега
Общо
0.00
€
|
0.00
лв.
Поръчай Поръчай
Поръчай
Поръчай
Имаш клиентски профил? Имаш клиентски профил?
Имаш клиентски профил?
Имаш клиентски профил?
Компанията
Компанията
За нас
За нас
Етика и съответствие
Етика и съответствие
Марката Vivacom
Марката Vivacom
Мениджмънт
Мениджмънт
Социална отговорност
Социална отговорност
Новини
Новини
Кариери
Кариери
Доставчици
Доставчици
Доклад за устойчиво развитие
Доклад за устойчиво развитие
Частни клиенти
Частни клиенти
Мобилни планове
Мобилни планове
Мобилен интернет
Мобилен интернет
Устройства
Устройства
Интернет пакети
Интернет пакети
Програма Лоялен клиент
Програма Лоялен клиент
Правила и условия
Правила и условия
Общи условия
Общи условия
Мобилно покритие
Мобилно покритие
Лични данни
Лични данни
Правила за ползване
Правила за ползване
Роуминг
Роуминг
Политика за бисквитките
Политика за бисквитките
Полезни връзки
Полезни връзки
Устройство в сервиз
Устройство в сервиз
Спешни номера
Спешни номера
Активиране на EON TV
Активиране на EON TV
Настройки на CA модул
Настройки на CA модул
Застраховки
Застраховки
Планове за хора с увреждания
Планове за хора с увреждания
Достъпност на сайта
Достъпност на сайта
Електронни фактури
Електронни фактури
EUR BGN
Валутен курс: 1 EUR = 1.95583 лв.
© VIVACOM 2026
google app - целевият уебсайт може да не е наличен
App store - отвори в нов раздел
Huawei store - отвори в нов раздел
Facebook
TikTok
YouTube
Instagram
Linkedin
United group - отвори в нов раздел
Отворена джаджа за чат
1
Open CMP widget
javascript:void(0)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9009
|
406
|
2
|
2026-05-08T11:56:39.942515+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778241399942_m2.jpg...
|
Firefox
|
Vivacom - 5G мобилни планове, телевизия EON и инте Vivacom - 5G мобилни планове, телевизия EON и интернет — Personal...
|
True
|
www.vivacom.bg
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
EON пакети TV и интернет
Pull requests · screenpip EON пакети TV и интернет
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Vivacom - 5G мобилни планове, телевизия EON и интернет
Vivacom - 5G мобилни планове, телевизия EON и интернет
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Премини към основното съдържание
Активиране на достъпност за хора със слабо зрение
Отворете менюто за достъпност
Настоящият сайт използва бисквитки
Настоящият сайт използва бисквитки
Използваме „бисквитки“ (cookies), за да персонализираме съдържанието и рекламите, да предоставяме функции на социални медии и да анализираме трафика си. Също така споделяме информация за начина, по който използвате сайта ни, с партньорските си социални медии, рекламните си партньори и партньори за анализ, които може да я комбинират с друга предоставена им от Вас информация или с такава, която са събрали от ползването от Ваша страна на услугите им. Молим да направите своя избор, след като се запознаете с детайлната информация за различните категории „бисквитки“ (cookies).
Настройки
Настройки
Позволи всички бисквитки
Персонализация
Персонализация
Само строго необходимите бисквитки
ЧАСТНИ КЛИЕНТИ
ЧАСТНИ КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
МАГАЗИНИ
МАГАЗИНИ
ГЛЕДАЙ EON
ГЛЕДАЙ EON
КОНТАКТИ
КОНТАКТИ
ОБЩИ УСЛОВИЯ
ОБЩИ УСЛОВИЯ
КАРТИ НА ПОКРИТИЕТО
КАРТИ НА ПОКРИТИЕТО...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"EON пакети TV и интернет","depth":2,"bounds":{"left":0.64162236,"top":0.27214685,"width":0.048038565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.26097074,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.27426863,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Home | Hostinger","depth":4,"bounds":{"left":0.26097074,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Home | Hostinger","depth":5,"bounds":{"left":0.27426863,"top":0.09577015,"width":0.03025266,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login – Nginx Proxy Manager","depth":4,"bounds":{"left":0.26097074,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login – Nginx Proxy Manager","depth":5,"bounds":{"left":0.27426863,"top":0.12849163,"width":0.05069814,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.26097074,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.27426863,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.26097074,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.27426863,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.26097074,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.27426863,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.26097074,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.27426863,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.26097074,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.27426863,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Vivacom - 5G мобилни планове, телевизия EON и интернет","depth":4,"bounds":{"left":0.26097074,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Vivacom - 5G мобилни планове, телевизия EON и интернет","depth":5,"bounds":{"left":0.27426863,"top":0.32482043,"width":0.10721409,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.36236703,"top":0.32083002,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.26379654,"top":0.34796488,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.26379654,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.27476728,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.28590426,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.29704124,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.3081782,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Премини към основното съдържание","depth":7,"bounds":{"left":0.38730052,"top":0.0,"width":0.10472074,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Активиране на достъпност за хора със слабо зрение","depth":7,"bounds":{"left":0.38730052,"top":0.0,"width":0.10472074,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Отворете менюто за достъпност","depth":7,"bounds":{"left":0.38730052,"top":0.0,"width":0.10472074,"height":0.05027933},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Настоящият сайт използва бисквитки","depth":11,"bounds":{"left":0.50049865,"top":0.8136473,"width":0.35023272,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Настоящият сайт използва бисквитки","depth":12,"bounds":{"left":0.50049865,"top":0.8168396,"width":0.09823803,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Използваме „бисквитки“ (cookies), за да персонализираме съдържанието и рекламите, да предоставяме функции на социални медии и да анализираме трафика си. Също така споделяме информация за начина, по който използвате сайта ни, с партньорските си социални медии, рекламните си партньори и партньори за анализ, които може да я комбинират с друга предоставена им от Вас информация или с такава, която са събрали от ползването от Ваша страна на услугите им. Молим да направите своя избор, след като се запознаете с детайлната информация за различните категории „бисквитки“ (cookies).","depth":12,"bounds":{"left":0.50049865,"top":0.84197927,"width":0.3487367,"height":0.088986434},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Настройки","depth":10,"bounds":{"left":0.50049865,"top":0.95690346,"width":0.03507314,"height":0.023942538},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Настройки","depth":11,"bounds":{"left":0.50049865,"top":0.9628891,"width":0.027260639,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Позволи всички бисквитки","depth":10,"bounds":{"left":0.85821146,"top":0.80885875,"width":0.09507979,"height":0.039505187},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Персонализация","depth":10,"bounds":{"left":0.85821146,"top":0.85434955,"width":0.09507979,"height":0.039505187},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Персонализация","depth":11,"bounds":{"left":0.88115025,"top":0.867917,"width":0.041722074,"height":0.012370312},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Само строго необходимите бисквитки","depth":10,"bounds":{"left":0.85821146,"top":0.89984035,"width":0.09507979,"height":0.0518755},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"ЧАСТНИ КЛИЕНТИ","depth":10,"bounds":{"left":0.41539228,"top":0.0518755,"width":0.0631649,"height":0.037110932},"on_screen":true,"help_text":"Частни клиенти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ЧАСТНИ КЛИЕНТИ","depth":11,"bounds":{"left":0.42337102,"top":0.061452515,"width":0.04720745,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"БИЗНЕС КЛИЕНТИ","depth":10,"bounds":{"left":0.47855717,"top":0.0518755,"width":0.062832445,"height":0.037110932},"on_screen":true,"help_text":"Бизнес клиенти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"БИЗНЕС КЛИЕНТИ","depth":11,"bounds":{"left":0.4865359,"top":0.061452515,"width":0.046875,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"МАГАЗИНИ","depth":10,"bounds":{"left":0.67985374,"top":0.0518755,"width":0.04488032,"height":0.037110932},"on_screen":true,"help_text":"Магазини","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"МАГАЗИНИ","depth":11,"bounds":{"left":0.6878325,"top":0.061452515,"width":0.028922873,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ГЛЕДАЙ EON","depth":10,"bounds":{"left":0.72473407,"top":0.0518755,"width":0.04837101,"height":0.037110932},"on_screen":true,"help_text":"Гледай EON","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ГЛЕДАЙ EON","depth":11,"bounds":{"left":0.73271275,"top":0.061452515,"width":0.032413565,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"КОНТАКТИ","depth":10,"bounds":{"left":0.773105,"top":0.0518755,"width":0.044049203,"height":0.037110932},"on_screen":true,"help_text":"Контакти","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"КОНТАКТИ","depth":11,"bounds":{"left":0.78108376,"top":0.061452515,"width":0.028091755,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"ОБЩИ УСЛОВИЯ","depth":10,"bounds":{"left":0.8171542,"top":0.0518755,"width":0.058011968,"height":0.037110932},"on_screen":true,"help_text":"Общи условия","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"ОБЩИ УСЛОВИЯ","depth":11,"bounds":{"left":0.82513297,"top":0.061452515,"width":0.042054523,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"КАРТИ НА ПОКРИТИЕТО","depth":10,"bounds":{"left":0.87516624,"top":0.0518755,"width":0.07829122,"height":0.037110932},"on_screen":true,"help_text":"Карти на покритието","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"КАРТИ НА ПОКРИТИЕТО","depth":11,"bounds":{"left":0.883145,"top":0.061452515,"width":0.062333778,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
3797946758247392048
|
-5012037588410579446
|
click
|
accessibility
|
NULL
|
EON пакети TV и интернет
Pull requests · screenpip EON пакети TV и интернет
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Home | Hostinger
Home | Hostinger
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Vivacom - 5G мобилни планове, телевизия EON и интернет
Vivacom - 5G мобилни планове, телевизия EON и интернет
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Премини към основното съдържание
Активиране на достъпност за хора със слабо зрение
Отворете менюто за достъпност
Настоящият сайт използва бисквитки
Настоящият сайт използва бисквитки
Използваме „бисквитки“ (cookies), за да персонализираме съдържанието и рекламите, да предоставяме функции на социални медии и да анализираме трафика си. Също така споделяме информация за начина, по който използвате сайта ни, с партньорските си социални медии, рекламните си партньори и партньори за анализ, които може да я комбинират с друга предоставена им от Вас информация или с такава, която са събрали от ползването от Ваша страна на услугите им. Молим да направите своя избор, след като се запознаете с детайлната информация за различните категории „бисквитки“ (cookies).
Настройки
Настройки
Позволи всички бисквитки
Персонализация
Персонализация
Само строго необходимите бисквитки
ЧАСТНИ КЛИЕНТИ
ЧАСТНИ КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
БИЗНЕС КЛИЕНТИ
МАГАЗИНИ
МАГАЗИНИ
ГЛЕДАЙ EON
ГЛЕДАЙ EON
КОНТАКТИ
КОНТАКТИ
ОБЩИ УСЛОВИЯ
ОБЩИ УСЛОВИЯ
КАРТИ НА ПОКРИТИЕТО
КАРТИ НА ПОКРИТИЕТО...
|
9007
|
NULL
|
NULL
|
NULL
|
|
13823
|
620
|
9
|
2026-05-09T16:35:59.210875+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778344559210_m2.jpg...
|
Firefox
|
lakylak/finance-hub - finance-hub - Gitea: Git wit lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea — Personal...
|
True
|
gitea.com/lakylak/finance-hub
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
EU alternatives to github : r/github
reddit.com
Pu EU alternatives to github : r/github
reddit.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Select: transactions - db - Adminer
Select: transactions - db - Adminer
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Close tab
EU alternatives to github : r/github
EU alternatives to github : r/github
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Dashboard
Issues
Issues
Pull Requests
Pull Requests
Milestones
Milestones
Explore
Explore
Notifications
lakylak
lakylak
/
finance-hub
finance-hub
Private
RSS Feed
Unwatch
1
1
Star
0
0
Fork
Fork
0
0
Code
Code
Issues
Issues
Pull Requests
Pull Requests
Actions
Actions
Projects
Projects
Releases
Releases
Wiki
Wiki
Activity
Activity
Settings
Settings
2 Commits
2
Commits
1 Branch
1
Branch
0 Tags
0
Tags
main
New Pull Request
Go to file
Code
Code
Lukas Kovalik
9a757fe67e
9a757fe67e
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
...
41 minutes ago
backend
backend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
frontend
frontend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
scripts
scripts
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.env.example
.env.example
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.gitignore
.gitignore
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
docker-compose.yml
docker-compose.yml
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
README.md
README.md
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
41 minutes ago
README.md Escape Edit File
README.md
README.md
Escape
Edit File
Finance Hub
Finance Hub
Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.
Replaces two separate tools:
payments-logger
— ingested DSK Bank SMS notifications
dsk-uploader
— parsed DSK Bank CSV exports and sent them to Notion
Features
Features
SMS ingest
— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance
CSV upload
— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)
Deduplication
— exact
rawMessage
match prevents duplicate imports from re-uploads or re-runs of the reimport script
Auto-tagging
— new imports for a known recipient are automatically tagged based on tag history for that payee
Rule-based tagging
— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)
Deduplication display
— same-day same-amount records from SMS and CSV are shown as a single "SMS + CSV" row in the UI
Notification forwarding
— sends payment summaries to a configurable notifier service (Viber, etc.)
Tag management
— create, colour-code, and remove tags on individual transactions
Settings
— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS
Auth
— delegated to Authentik via NPM reverse proxy header injection; no local user accounts
Tech Stack
Tech Stack
Layer
Technology
Backend
Node.js 20, Express, Prisma 5
Database
PostgreSQL 16
CSV parsing
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
File upload
multer
(memory storage)
Frontend
React 18, Vite, Tailwind CSS, Lucide React
Auth
Authentik forward-auth via
X-authentik-username
header
Container
Docker Compose
Layer
Backend
Database
CSV parsing
File upload
Frontend
Auth
Container
Technology
Node.js 20, Express, Prisma 5
PostgreSQL 16
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
multer
(memory storage)
React 18, Vite, Tailwind CSS, Lucide React
Authentik forward-auth via
X-authentik-username
header
Docker Compose
Quick Start
Quick Start
1. Clone and configure
1. Clone and configure
git clone [EMAIL]:lakylak/finance-hub.git
cd
finance-hub
cp .env.example .env
# Edit .env — set DB_PASSWORD and notifier settings
2. Start
2. Start
docker compose up -d --build
Services:
Service
Default port
Frontend
5175
Backend API
3001
Adminer (DB UI)
8092
Service
Frontend
Backend API
Adminer (DB UI)
Default port
5175
3001
8092
3. First run
3. First run
Prisma migrations run automatically on backend startup via
prisma migrate deploy
.
Environment variables
Environment variables
Variable
Required
Description
DB_PASSWORD
Yes
PostgreSQL password
NOTIFIER_URL
No
Base URL of the notifier service
NOTIFIER_CHANNEL
No
Channel to use (
viber
, etc.) Default:
viber
NOTIFY_DEFAULT_PHONE
No
Phone number for payment notifications
TZ
No
Timezone for SMS date parsing. Default:
Europe/Sofia
BACKEND_PORT
No
Backend listen port. Default:
3001
FRONTEND_PORT
No
Frontend listen port. Default:
5175
DEV_BYPASS_AUTH
No
Set
true
to skip Authentik header check during local dev
Variable
DB_PASSWORD
NOTIFIER_URL
NOTIFIER_CHANNEL
NOTIFY_DEFAULT_PHONE
TZ
BACKEND_PORT
FRONTEND_PORT
DEV_BYPASS_AUTH
Required
Yes
No
No
No
No
No
No
No
Description
PostgreSQL password
Base URL of the notifier service
Channel to use (
viber
, etc.) Default:
viber
Phone number for payment notifications
Timezone for SMS date parsing. Default:
Europe/Sofia
Backend listen port. Default:
3001
Frontend listen port. Default:
5175
Set
true
to skip Authentik header check during local dev
Authentication
Authentication
In production, route the frontend through
Nginx Proxy Manager
with an
Authentik forward-auth
provider. NPM injects
X-authentik-username
(and optionally
X-authentik-email
,
X-authentik-groups
) into every proxied request.
The backend reads these headers and rejects requests without them (unless
DEV_BYPASS_AUTH=true
).
The public endpoints below are exempt and require no authentication:
GET /api/health
POST /api/payments/ingest
Sign-out link:
/outpost.goauthentik.io/sign_out
API Reference
API Reference
Health
Health
GET /api/health
GET /api/health
Public. Returns service status and database connection info.
{
"status"
:
"ok"
,
"timestamp"
:
"2026-05-09T10:00:00.000Z"
,
"storage"
:
{
"type"
:
"PostgreSQL"
,
"host"
:
"db"
,
"database"
:
"finance_hub"
}
}
Payments (Transaction Imports)
Payments (Transaction Imports)
Base path:
/api/payments
All endpoints require authentication except
/ingest
.
POST /api/payments/ingest
POST /api/payments/ingest
Public.
Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.
Rate limited to
200 requests/minute
.
Returns
409
if the exact message was already imported.
SMS body:
{
"message"
:
"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR."
}
Structured (Apple Wallet) body:
{
"ingestMode"
:
"apple_wallet"
,
"amount"
:
12.50
,
"recipient"
:
"Starbucks"
,
"type"
:
"WALLET"
,
"card"
:
"4444"
,
"date"
:
"2026-05-09T10:30:00Z"
,
"balance"
:
null
}
Response:
201 Created
— the created import record.
Duplicate:
409 Conflict
—
{ "error": "Already imported", "existing": { ... } }
GET /api/payments
GET /api/payments
List transaction imports with filtering, sorting, and pagination.
Query parameters:
Parameter
Type
Description
page
int
Page number. Default:
1
limit
int
Records per page, max 200. Default:
50
sortBy
string
Field to sort by:
date
,
amount
,
balance
,
recipient
,
type
,
source
,
createdAt
,
status
. Default:
createdAt
sortDir
string
asc
or
desc
. Default:
desc
status
string
Filter by status:
UNPROCESSED
,
SENT
,
SKIPPED
source
string
Filter by source:
INGEST
,
UPLOAD
type
string
Filter by transaction type (e.g.
POS
,
ATM
)
tag
string
Filter by tag name
recipient
string
Case-insensitive substring match on recipient
search
string
Case-insensitive search across...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"EU alternatives to github : r/github","depth":4,"bounds":{"left":0.11768617,"top":0.78451717,"width":0.0625,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"reddit.com","depth":4,"bounds":{"left":0.11768617,"top":0.7952913,"width":0.018783245,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.041101355,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.05227454,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DNS / Nameservers | Hostinger","depth":4,"bounds":{"left":0.0,"top":0.073822826,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DNS / Nameservers | Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.08499601,"width":0.053856384,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.10654429,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.117717475,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.13926576,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.15043895,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.17198724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.18316041,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.2047087,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.21588188,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.23743017,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.24860336,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.27015164,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.28132483,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.3028731,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.3140463,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.33559456,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.34676775,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.36831605,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.3794892,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Your old PC can run Windows 11 in a VM, but not on bare metal - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.4010375,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Your old PC can run Windows 11 in a VM, but not on bare metal - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.4122107,"width":0.16855054,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"(25) Quora","depth":4,"bounds":{"left":0.0,"top":0.43375897,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(25) Quora","depth":5,"bounds":{"left":0.013297873,"top":0.44493216,"width":0.018949468,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.46648043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.47765362,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.49920192,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.5103751,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.5319234,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.54309654,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Select: transactions - db - Adminer","depth":4,"bounds":{"left":0.0,"top":0.5646449,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Select: transactions - db - Adminer","depth":5,"bounds":{"left":0.013297873,"top":0.57581806,"width":0.061170213,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":4,"bounds":{"left":0.0,"top":0.59736633,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":5,"bounds":{"left":0.013297873,"top":0.6085395,"width":0.09059176,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.6300878,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.641261,"width":0.113696806,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"bounds":{"left":0.0,"top":0.66280925,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"bounds":{"left":0.013297873,"top":0.67398244,"width":0.016788565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":4,"bounds":{"left":0.0,"top":0.6955307,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":5,"bounds":{"left":0.013297873,"top":0.7067039,"width":0.10239362,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"bounds":{"left":0.0,"top":0.7282522,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"bounds":{"left":0.013297873,"top":0.73942536,"width":0.016788565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":4,"bounds":{"left":0.0,"top":0.7609737,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":5,"bounds":{"left":0.013297873,"top":0.7721468,"width":0.098902926,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude Code | Claude Platform","depth":4,"bounds":{"left":0.0,"top":0.79369515,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude Code | Claude Platform","depth":5,"bounds":{"left":0.013297873,"top":0.80486834,"width":0.053357713,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude","depth":4,"bounds":{"left":0.0,"top":0.8264166,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude","depth":5,"bounds":{"left":0.013297873,"top":0.8375898,"width":0.012134309,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea","depth":4,"bounds":{"left":0.0,"top":0.8591381,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea","depth":5,"bounds":{"left":0.013297873,"top":0.87031126,"width":0.10920878,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.86632085,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"EU alternatives to github : r/github","depth":4,"bounds":{"left":0.0,"top":0.89185953,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EU alternatives to github : r/github","depth":5,"bounds":{"left":0.013297873,"top":0.9030327,"width":0.059674203,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.8990423,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.92897046,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Dashboard","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Issues","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull Requests","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull Requests","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Milestones","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Milestones","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Explore","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Explore","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Notifications","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"lakylak","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"lakylak","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"finance-hub","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"finance-hub","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Private","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RSS Feed","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unwatch","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"1","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Star","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"0","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Fork","depth":8,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fork","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Issues","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull Requests","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull Requests","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Projects","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Projects","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Releases","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Releases","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Activity","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Activity","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 Commits","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commits","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1 Branch","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Branch","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Tags","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tags","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"main","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Pull Request","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Go to file","depth":9,"on_screen":false,"help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"9a757fe67e","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9a757fe67e","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add README with project description, API reference, and usage guide","depth":11,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add README with project description, API reference, and usage guide","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"...","depth":10,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"41 minutes ago","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"backend","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"backend","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frontend","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frontend","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"scripts","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"scripts","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".env.example","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".env.example","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".gitignore","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".gitignore","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"docker-compose.yml","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"README.md","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"README.md","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add README with project description, API reference, and usage guide","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add README with project description, API reference, and usage guide","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"41 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"README.md Escape Edit File","depth":8,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"README.md","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"README.md","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Escape","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Edit File","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Finance Hub","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Finance Hub","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Replaces two separate tools:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"payments-logger","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— ingested DSK Bank SMS notifications","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dsk-uploader","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— parsed DSK Bank CSV exports and sent them to Notion","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Features","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Features","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SMS ingest","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deduplication","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— exact","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rawMessage","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"match prevents duplicate imports from re-uploads or re-runs of the reimport script","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto-tagging","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— new imports for a known recipient are automatically tagged based on tag history for that payee","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Rule-based tagging","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deduplication display","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— same-day same-amount records from SMS and CSV are shown as a single \"SMS + CSV\" row in the UI","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notification forwarding","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— sends payment summaries to a configurable notifier service (Viber, etc.)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tag management","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— create, colour-code, and remove tags on individual transactions","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Settings","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— delegated to Authentik via NPM reverse proxy header injection; no local user accounts","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Tech Stack","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tech Stack","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layer","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Technology","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Node.js 20, Express, Prisma 5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Database","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL 16","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV parsing","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"csv-parse","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iconv-lite","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(cp1251 + UTF-8 BOM)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"File upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"multer","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(memory storage)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"React 18, Vite, Tailwind CSS, Lucide React","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth via","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"header","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Container","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Docker Compose","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layer","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Database","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV parsing","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"File upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Container","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Technology","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Node.js 20, Express, Prisma 5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL 16","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"csv-parse","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iconv-lite","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(cp1251 + UTF-8 BOM)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"multer","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(memory storage)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"React 18, Vite, Tailwind CSS, Lucide React","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth via","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"header","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Docker Compose","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Quick Start","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Quick Start","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"1. Clone and configure","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Clone and configure","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"git clone git@gitea.com:lakylak/finance-hub.git","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cd","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"finance-hub\ncp .env.example .env","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"# Edit .env — set DB_PASSWORD and notifier settings","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"2. Start","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. Start","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"docker compose up -d --build","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Services:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Default port","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend API","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Adminer (DB UI)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8092","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend API","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Adminer (DB UI)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Default port","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8092","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"3. First run","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. First run","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Prisma migrations run automatically on backend startup via","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prisma migrate deploy","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Environment variables","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Environment variables","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Variable","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Required","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB_PASSWORD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Yes","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL password","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_URL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base URL of the notifier service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_CHANNEL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Channel to use (","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.) Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFY_DEFAULT_PHONE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Phone number for payment notifications","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TZ","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Timezone for SMS date parsing. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Europe/Sofia","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BACKEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FRONTEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Set","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to skip Authentik header check during local dev","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Variable","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB_PASSWORD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_URL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_CHANNEL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFY_DEFAULT_PHONE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TZ","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BACKEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FRONTEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Required","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Yes","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL password","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base URL of the notifier service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Channel to use (","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.) Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Phone number for payment notifications","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Timezone for SMS date parsing. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Europe/Sofia","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Set","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to skip Authentik header check during local dev","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Authentication","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentication","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"In production, route the frontend through","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"with an","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider. NPM injects","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and optionally","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-email","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-groups","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") into every proxied request.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The backend reads these headers and rejects requests without them (unless","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH=true","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":").","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The public endpoints below are exempt and require no authentication:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/health","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POST /api/payments/ingest","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Sign-out link:","depth":10,"bounds":{"left":0.12965426,"top":0.0,"width":0.03357713,"height":0.015163607},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/outpost.goauthentik.io/sign_out","depth":11,"bounds":{"left":0.16505983,"top":0.0,"width":0.087101065,"height":0.0131683955},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"API Reference","depth":9,"bounds":{"left":0.12965426,"top":0.0,"width":0.25598404,"height":0.030327214},"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"API Reference","depth":10,"bounds":{"left":0.12965426,"top":0.0,"width":0.051030584,"height":0.023144454},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Health","depth":9,"bounds":{"left":0.12965426,"top":0.017956903,"width":0.25598404,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Health","depth":10,"bounds":{"left":0.12965426,"top":0.018355945,"width":0.020113032,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"GET /api/health","depth":9,"bounds":{"left":0.12965426,"top":0.057063047,"width":0.25598404,"height":0.016360734},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/health","depth":11,"bounds":{"left":0.13181517,"top":0.057861134,"width":0.048038565,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Public. Returns service status and database connection info.","depth":10,"bounds":{"left":0.12965426,"top":0.087789305,"width":0.14594415,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"bounds":{"left":0.1349734,"top":0.13128492,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"status\"","depth":11,"bounds":{"left":0.14045878,"top":0.14724661,"width":0.021775266,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.16223404,"top":0.14724661,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"ok\"","depth":11,"bounds":{"left":0.16755319,"top":0.14724661,"width":0.010970744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.17852394,"top":0.14724661,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"timestamp\"","depth":11,"bounds":{"left":0.14045878,"top":0.16280925,"width":0.029920213,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.17037898,"top":0.16280925,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"2026-05-09T10:00:00.000Z\"","depth":11,"bounds":{"left":0.17586437,"top":0.16280925,"width":0.070644945,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.24650931,"top":0.16280925,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"storage\"","depth":11,"bounds":{"left":0.14045878,"top":0.1783719,"width":0.02443484,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.16489361,"top":0.1783719,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"bounds":{"left":0.17037898,"top":0.1783719,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"type\"","depth":11,"bounds":{"left":0.17586437,"top":0.1783719,"width":0.016289894,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.19215426,"top":0.1783719,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"PostgreSQL\"","depth":11,"bounds":{"left":0.1974734,"top":0.1783719,"width":0.03274601,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.23021941,"top":0.1783719,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"host\"","depth":11,"bounds":{"left":0.2357048,"top":0.1783719,"width":0.016289894,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.25199467,"top":0.1783719,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"db\"","depth":11,"bounds":{"left":0.25748006,"top":0.1783719,"width":0.010804521,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.2682846,"top":0.1783719,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"database\"","depth":11,"bounds":{"left":0.27376994,"top":0.1783719,"width":0.027094414,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.30086437,"top":0.1783719,"width":0.0028257978,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"finance_hub\"","depth":11,"bounds":{"left":0.30634972,"top":0.1783719,"width":0.035405584,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"bounds":{"left":0.3444149,"top":0.1783719,"width":0.0028257978,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"bounds":{"left":0.1349734,"top":0.1943336,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Payments (Transaction Imports)","depth":9,"bounds":{"left":0.12965426,"top":0.2565842,"width":0.25598404,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Payments (Transaction Imports)","depth":10,"bounds":{"left":0.12965426,"top":0.25698325,"width":0.09840426,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base path:","depth":10,"bounds":{"left":0.12965426,"top":0.29090184,"width":0.027094414,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/api/payments","depth":11,"bounds":{"left":0.15857713,"top":0.29289705,"width":0.035405584,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"All endpoints require authentication except","depth":10,"bounds":{"left":0.12965426,"top":0.30925778,"width":0.10488697,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ingest","depth":11,"bounds":{"left":0.23636968,"top":0.31125298,"width":0.018949468,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.2571476,"top":0.30925778,"width":0.0014960107,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"POST /api/payments/ingest","depth":9,"bounds":{"left":0.12965426,"top":0.36113328,"width":0.25598404,"height":0.016360734},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POST /api/payments/ingest","depth":11,"bounds":{"left":0.13181517,"top":0.36193135,"width":0.079953454,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Public.","depth":11,"bounds":{"left":0.12965426,"top":0.3914605,"width":0.017287234,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.","depth":10,"bounds":{"left":0.14694148,"top":0.3914605,"width":0.1796875,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Rate limited to","depth":10,"bounds":{"left":0.12965426,"top":0.4225858,"width":0.036236703,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"200 requests/minute","depth":11,"bounds":{"left":0.16589096,"top":0.4225858,"width":0.05285904,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.21875,"top":0.4225858,"width":0.0013297872,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Returns","depth":10,"bounds":{"left":0.12965426,"top":0.44094175,"width":0.020279255,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"409","depth":11,"bounds":{"left":0.15159574,"top":0.44293696,"width":0.008144947,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if the exact message was already imported.","depth":10,"bounds":{"left":0.16156915,"top":0.44094175,"width":0.10638298,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SMS body:","depth":11,"bounds":{"left":0.12965426,"top":0.471668,"width":0.027260639,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"bounds":{"left":0.1349734,"top":0.51556265,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"message\"","depth":11,"bounds":{"left":0.14045878,"top":0.51556265,"width":0.02443484,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.16489361,"top":0.51556265,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR.\"","depth":11,"bounds":{"left":0.17037898,"top":0.51556265,"width":0.40525267,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"bounds":{"left":0.5784575,"top":0.51556265,"width":0.0026595744,"height":0.0131683955},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Structured (Apple Wallet) body:","depth":11,"bounds":{"left":0.12965426,"top":0.55706304,"width":0.08028591,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"bounds":{"left":0.1349734,"top":0.60055864,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"ingestMode\"","depth":11,"bounds":{"left":0.14045878,"top":0.6161213,"width":0.032579787,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.17303856,"top":0.6161213,"width":0.0028257978,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"apple_wallet\"","depth":11,"bounds":{"left":0.17852394,"top":0.6161213,"width":0.038065158,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.2165891,"top":0.6161213,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"amount\"","depth":11,"bounds":{"left":0.14045878,"top":0.632083,"width":0.021775266,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.16223404,"top":0.632083,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.50","depth":11,"bounds":{"left":0.16755319,"top":0.632083,"width":0.013630319,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.18118352,"top":0.632083,"width":0.0028257978,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"recipient\"","depth":11,"bounds":{"left":0.14045878,"top":0.64764565,"width":0.029920213,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.17037898,"top":0.64764565,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"Starbucks\"","depth":11,"bounds":{"left":0.17586437,"top":0.64764565,"width":0.029920213,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.20578457,"top":0.64764565,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"type\"","depth":11,"bounds":{"left":0.14045878,"top":0.6632083,"width":0.016289894,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.15674867,"top":0.6632083,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"WALLET\"","depth":11,"bounds":{"left":0.16223404,"top":0.6632083,"width":0.021775266,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.18400931,"top":0.6632083,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"card\"","depth":11,"bounds":{"left":0.14045878,"top":0.67917,"width":0.016289894,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.15674867,"top":0.67917,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"4444\"","depth":11,"bounds":{"left":0.16223404,"top":0.67917,"width":0.016289894,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.17852394,"top":0.67917,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"date\"","depth":11,"bounds":{"left":0.14045878,"top":0.69473267,"width":0.016289894,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.15674867,"top":0.69473267,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"2026-05-09T10:30:00Z\"","depth":11,"bounds":{"left":0.16223404,"top":0.69473267,"width":0.059840426,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.22207446,"top":0.69473267,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"balance\"","depth":11,"bounds":{"left":0.14045878,"top":0.7106943,"width":0.02443484,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.16489361,"top":0.7106943,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":11,"bounds":{"left":0.17037898,"top":0.7106943,"width":0.010804521,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"bounds":{"left":0.1349734,"top":0.72625697,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Response:","depth":11,"bounds":{"left":0.12965426,"top":0.76775736,"width":0.02642952,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"201 Created","depth":11,"bounds":{"left":0.15940824,"top":0.7697526,"width":0.029920213,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— the created import record.","depth":10,"bounds":{"left":0.19099069,"top":0.76775736,"width":0.07114362,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Duplicate:","depth":11,"bounds":{"left":0.12965426,"top":0.7861133,"width":0.025598405,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"409 Conflict","depth":11,"bounds":{"left":0.15857713,"top":0.7881085,"width":0.032579787,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":10,"bounds":{"left":0.19298537,"top":0.7861133,"width":0.007480053,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{ \"error\": \"Already imported\", \"existing\": { ... } }","depth":11,"bounds":{"left":0.20229389,"top":0.7881085,"width":0.14145611,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"GET /api/payments","depth":9,"bounds":{"left":0.12965426,"top":0.83798885,"width":0.25598404,"height":0.016360734},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/payments","depth":11,"bounds":{"left":0.13181517,"top":0.8387869,"width":0.05435505,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"List transaction imports with filtering, sorting, and pagination.","depth":10,"bounds":{"left":0.12965426,"top":0.8687151,"width":0.14827128,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Query parameters:","depth":11,"bounds":{"left":0.12965426,"top":0.89944136,"width":0.04737367,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Parameter","depth":12,"bounds":{"left":0.14611037,"top":0.9365523,"width":0.026263298,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.19647606,"top":0.9365523,"width":0.012466756,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"bounds":{"left":0.28656915,"top":0.9365523,"width":0.029089095,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"page","depth":13,"bounds":{"left":0.13613696,"top":0.9680766,"width":0.010804521,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"int","depth":12,"bounds":{"left":0.1931516,"top":0.9660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Page number. Default:","depth":12,"bounds":{"left":0.22140957,"top":0.9660814,"width":0.05501995,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":13,"bounds":{"left":0.27825797,"top":0.9680766,"width":0.0026595744,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit","depth":13,"bounds":{"left":0.13613696,"top":0.99760574,"width":0.013630319,"height":0.002394259},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"int","depth":12,"bounds":{"left":0.1931516,"top":0.99561054,"width":0.005984043,"height":0.004389465},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Records per page, max 200. Default:","depth":12,"bounds":{"left":0.22140957,"top":0.99561054,"width":0.08992686,"height":0.004389465},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"50","depth":13,"bounds":{"left":0.3131649,"top":0.99760574,"width":0.005485372,"height":0.002394259},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortBy","depth":13,"bounds":{"left":0.13613696,"top":1.0,"width":0.016289894,"height":-0.03671193},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"bounds":{"left":0.1931516,"top":1.0,"width":0.013796543,"height":-0.034716725},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Field to sort by:","depth":12,"bounds":{"left":0.22140957,"top":1.0,"width":0.0390625,"height":-0.02513969},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":13,"bounds":{"left":0.26230052,"top":1.0,"width":0.010804521,"height":-0.027134895},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.27493352,"top":1.0,"width":0.0029920214,"height":-0.02513969},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":13,"bounds":{"left":0.27975398,"top":1.0,"width":0.016289894,"height":-0.027134895},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.29787233,"top":1.0,"width":0.0029920214,"height":-0.02513969},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"balance","depth":13,"bounds":{"left":0.30269283,"top":1.0,"width":0.018949468,"height":-0.027134895},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.32347074,"top":1.0,"width":0.0029920214,"height":-0.02513969},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"bounds":{"left":0.32829124,"top":1.0,"width":0.02443484,"height":-0.027134895},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.35455453,"top":1.0,"width":0.0029920214,"height":-0.02513969},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"bounds":{"left":0.359375,"top":1.0,"width":0.010804521,"height":-0.027134895},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.37200797,"top":1.0,"width":0.0014960107,"height":-0.02513969},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"bounds":{"left":0.22323804,"top":1.0,"width":0.016289894,"height":-0.046288848},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.24135639,"top":1.0,"width":0.0029920214,"height":-0.044293642},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createdAt","depth":13,"bounds":{"left":0.24601063,"top":1.0,"width":0.024601065,"height":-0.046288848},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.27244017,"top":1.0,"width":0.0028257978,"height":-0.044293642},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"bounds":{"left":0.27709442,"top":1.0,"width":0.016289894,"height":-0.046288848},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". Default:","depth":12,"bounds":{"left":0.29521278,"top":1.0,"width":0.023271276,"height":-0.044293642},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createdAt","depth":13,"bounds":{"left":0.3203125,"top":1.0,"width":0.024601065,"height":-0.046288848},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortDir","depth":13,"bounds":{"left":0.13613696,"top":1.0,"width":0.018949468,"height":-0.07581806},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"bounds":{"left":0.1931516,"top":1.0,"width":0.013796543,"height":-0.073822856},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"asc","depth":13,"bounds":{"left":0.22323804,"top":1.0,"width":0.008144947,"height":-0.07581806},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"or","depth":12,"bounds":{"left":0.23321144,"top":1.0,"width":0.0078125,"height":-0.073822856},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"desc","depth":13,"bounds":{"left":0.24285239,"top":1.0,"width":0.010970744,"height":-0.07581806},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". Default:","depth":12,"bounds":{"left":0.2556516,"top":1.0,"width":0.023271276,"height":-0.073822856},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"desc","depth":13,"bounds":{"left":0.2805851,"top":1.0,"width":0.010970744,"height":-0.07581806},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by status:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"UNPROCESSED","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SENT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SKIPPED","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by source:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INGEST","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"UPLOAD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by transaction type (e.g.","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POS","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ATM","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"tag","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by tag name","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Case-insensitive substring match on recipient","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"search","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Case-insensitive search across","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-3425750056780037068
|
4487445851658586390
|
click
|
accessibility
|
NULL
|
EU alternatives to github : r/github
reddit.com
Pu EU alternatives to github : r/github
reddit.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Select: transactions - db - Adminer
Select: transactions - db - Adminer
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Close tab
EU alternatives to github : r/github
EU alternatives to github : r/github
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Dashboard
Issues
Issues
Pull Requests
Pull Requests
Milestones
Milestones
Explore
Explore
Notifications
lakylak
lakylak
/
finance-hub
finance-hub
Private
RSS Feed
Unwatch
1
1
Star
0
0
Fork
Fork
0
0
Code
Code
Issues
Issues
Pull Requests
Pull Requests
Actions
Actions
Projects
Projects
Releases
Releases
Wiki
Wiki
Activity
Activity
Settings
Settings
2 Commits
2
Commits
1 Branch
1
Branch
0 Tags
0
Tags
main
New Pull Request
Go to file
Code
Code
Lukas Kovalik
9a757fe67e
9a757fe67e
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
...
41 minutes ago
backend
backend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
frontend
frontend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
scripts
scripts
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.env.example
.env.example
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.gitignore
.gitignore
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
docker-compose.yml
docker-compose.yml
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
README.md
README.md
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
41 minutes ago
README.md Escape Edit File
README.md
README.md
Escape
Edit File
Finance Hub
Finance Hub
Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.
Replaces two separate tools:
payments-logger
— ingested DSK Bank SMS notifications
dsk-uploader
— parsed DSK Bank CSV exports and sent them to Notion
Features
Features
SMS ingest
— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance
CSV upload
— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)
Deduplication
— exact
rawMessage
match prevents duplicate imports from re-uploads or re-runs of the reimport script
Auto-tagging
— new imports for a known recipient are automatically tagged based on tag history for that payee
Rule-based tagging
— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)
Deduplication display
— same-day same-amount records from SMS and CSV are shown as a single "SMS + CSV" row in the UI
Notification forwarding
— sends payment summaries to a configurable notifier service (Viber, etc.)
Tag management
— create, colour-code, and remove tags on individual transactions
Settings
— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS
Auth
— delegated to Authentik via NPM reverse proxy header injection; no local user accounts
Tech Stack
Tech Stack
Layer
Technology
Backend
Node.js 20, Express, Prisma 5
Database
PostgreSQL 16
CSV parsing
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
File upload
multer
(memory storage)
Frontend
React 18, Vite, Tailwind CSS, Lucide React
Auth
Authentik forward-auth via
X-authentik-username
header
Container
Docker Compose
Layer
Backend
Database
CSV parsing
File upload
Frontend
Auth
Container
Technology
Node.js 20, Express, Prisma 5
PostgreSQL 16
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
multer
(memory storage)
React 18, Vite, Tailwind CSS, Lucide React
Authentik forward-auth via
X-authentik-username
header
Docker Compose
Quick Start
Quick Start
1. Clone and configure
1. Clone and configure
git clone [EMAIL]:lakylak/finance-hub.git
cd
finance-hub
cp .env.example .env
# Edit .env — set DB_PASSWORD and notifier settings
2. Start
2. Start
docker compose up -d --build
Services:
Service
Default port
Frontend
5175
Backend API
3001
Adminer (DB UI)
8092
Service
Frontend
Backend API
Adminer (DB UI)
Default port
5175
3001
8092
3. First run
3. First run
Prisma migrations run automatically on backend startup via
prisma migrate deploy
.
Environment variables
Environment variables
Variable
Required
Description
DB_PASSWORD
Yes
PostgreSQL password
NOTIFIER_URL
No
Base URL of the notifier service
NOTIFIER_CHANNEL
No
Channel to use (
viber
, etc.) Default:
viber
NOTIFY_DEFAULT_PHONE
No
Phone number for payment notifications
TZ
No
Timezone for SMS date parsing. Default:
Europe/Sofia
BACKEND_PORT
No
Backend listen port. Default:
3001
FRONTEND_PORT
No
Frontend listen port. Default:
5175
DEV_BYPASS_AUTH
No
Set
true
to skip Authentik header check during local dev
Variable
DB_PASSWORD
NOTIFIER_URL
NOTIFIER_CHANNEL
NOTIFY_DEFAULT_PHONE
TZ
BACKEND_PORT
FRONTEND_PORT
DEV_BYPASS_AUTH
Required
Yes
No
No
No
No
No
No
No
Description
PostgreSQL password
Base URL of the notifier service
Channel to use (
viber
, etc.) Default:
viber
Phone number for payment notifications
Timezone for SMS date parsing. Default:
Europe/Sofia
Backend listen port. Default:
3001
Frontend listen port. Default:
5175
Set
true
to skip Authentik header check during local dev
Authentication
Authentication
In production, route the frontend through
Nginx Proxy Manager
with an
Authentik forward-auth
provider. NPM injects
X-authentik-username
(and optionally
X-authentik-email
,
X-authentik-groups
) into every proxied request.
The backend reads these headers and rejects requests without them (unless
DEV_BYPASS_AUTH=true
).
The public endpoints below are exempt and require no authentication:
GET /api/health
POST /api/payments/ingest
Sign-out link:
/outpost.goauthentik.io/sign_out
API Reference
API Reference
Health
Health
GET /api/health
GET /api/health
Public. Returns service status and database connection info.
{
"status"
:
"ok"
,
"timestamp"
:
"2026-05-09T10:00:00.000Z"
,
"storage"
:
{
"type"
:
"PostgreSQL"
,
"host"
:
"db"
,
"database"
:
"finance_hub"
}
}
Payments (Transaction Imports)
Payments (Transaction Imports)
Base path:
/api/payments
All endpoints require authentication except
/ingest
.
POST /api/payments/ingest
POST /api/payments/ingest
Public.
Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.
Rate limited to
200 requests/minute
.
Returns
409
if the exact message was already imported.
SMS body:
{
"message"
:
"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR."
}
Structured (Apple Wallet) body:
{
"ingestMode"
:
"apple_wallet"
,
"amount"
:
12.50
,
"recipient"
:
"Starbucks"
,
"type"
:
"WALLET"
,
"card"
:
"4444"
,
"date"
:
"2026-05-09T10:30:00Z"
,
"balance"
:
null
}
Response:
201 Created
— the created import record.
Duplicate:
409 Conflict
—
{ "error": "Already imported", "existing": { ... } }
GET /api/payments
GET /api/payments
List transaction imports with filtering, sorting, and pagination.
Query parameters:
Parameter
Type
Description
page
int
Page number. Default:
1
limit
int
Records per page, max 200. Default:
50
sortBy
string
Field to sort by:
date
,
amount
,
balance
,
recipient
,
type
,
source
,
createdAt
,
status
. Default:
createdAt
sortDir
string
asc
or
desc
. Default:
desc
status
string
Filter by status:
UNPROCESSED
,
SENT
,
SKIPPED
source
string
Filter by source:
INGEST
,
UPLOAD
type
string
Filter by transaction type (e.g.
POS
,
ATM
)
tag
string
Filter by tag name
recipient
string
Case-insensitive substring match on recipient
search
string
Case-insensitive search across...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
13824
|
619
|
6
|
2026-05-09T16:35:59.210886+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778344559210_m1.jpg...
|
Firefox
|
lakylak/finance-hub - finance-hub - Gitea: Git wit lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea — Personal...
|
True
|
gitea.com/lakylak/finance-hub
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
EU alternatives to github : r/github
reddit.com
Pu EU alternatives to github : r/github
reddit.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Select: transactions - db - Adminer
Select: transactions - db - Adminer
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Close tab
EU alternatives to github : r/github
EU alternatives to github : r/github
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Dashboard
Issues
Issues
Pull Requests
Pull Requests
Milestones
Milestones
Explore
Explore
Notifications
lakylak
lakylak
/
finance-hub
finance-hub
Private
RSS Feed
Unwatch
1
1
Star
0
0
Fork
Fork
0
0
Code
Code
Issues
Issues
Pull Requests
Pull Requests
Actions
Actions
Projects
Projects
Releases
Releases
Wiki
Wiki
Activity
Activity
Settings
Settings
2 Commits
2
Commits
1 Branch
1
Branch
0 Tags
0
Tags
main
New Pull Request
Go to file
Code
Code
Lukas Kovalik
9a757fe67e
9a757fe67e
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
...
41 minutes ago
backend
backend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
frontend
frontend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
scripts
scripts
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.env.example
.env.example
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.gitignore
.gitignore
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
docker-compose.yml
docker-compose.yml
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
README.md
README.md
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
41 minutes ago
README.md Escape Edit File
README.md
README.md
Escape
Edit File
Finance Hub
Finance Hub
Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.
Replaces two separate tools:
payments-logger
— ingested DSK Bank SMS notifications
dsk-uploader
— parsed DSK Bank CSV exports and sent them to Notion
Features
Features
SMS ingest
— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance
CSV upload
— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)
Deduplication
— exact
rawMessage
match prevents duplicate imports from re-uploads or re-runs of the reimport script
Auto-tagging
— new imports for a known recipient are automatically tagged based on tag history for that payee
Rule-based tagging
— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)
Deduplication display
— same-day same-amount records from SMS and CSV are shown as a single "SMS + CSV" row in the UI
Notification forwarding
— sends payment summaries to a configurable notifier service (Viber, etc.)
Tag management
— create, colour-code, and remove tags on individual transactions
Settings
— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS
Auth
— delegated to Authentik via NPM reverse proxy header injection; no local user accounts
Tech Stack
Tech Stack
Layer
Technology
Backend
Node.js 20, Express, Prisma 5
Database
PostgreSQL 16
CSV parsing
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
File upload
multer
(memory storage)
Frontend
React 18, Vite, Tailwind CSS, Lucide React
Auth
Authentik forward-auth via
X-authentik-username
header
Container
Docker Compose
Layer
Backend
Database
CSV parsing
File upload
Frontend
Auth
Container
Technology
Node.js 20, Express, Prisma 5
PostgreSQL 16
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
multer
(memory storage)
React 18, Vite, Tailwind CSS, Lucide React
Authentik forward-auth via
X-authentik-username
header
Docker Compose
Quick Start
Quick Start
1. Clone and configure
1. Clone and configure
git clone [EMAIL]:lakylak/finance-hub.git
cd
finance-hub
cp .env.example .env
# Edit .env — set DB_PASSWORD and notifier settings
2. Start
2. Start
docker compose up -d --build
Services:
Service
Default port
Frontend
5175
Backend API
3001
Adminer (DB UI)
8092
Service
Frontend
Backend API
Adminer (DB UI)
Default port
5175
3001
8092
3. First run
3. First run
Prisma migrations run automatically on backend startup via
prisma migrate deploy
.
Environment variables
Environment variables
Variable
Required
Description
DB_PASSWORD
Yes
PostgreSQL password
NOTIFIER_URL
No
Base URL of the notifier service
NOTIFIER_CHANNEL
No
Channel to use (
viber
, etc.) Default:
viber
NOTIFY_DEFAULT_PHONE
No
Phone number for payment notifications
TZ
No
Timezone for SMS date parsing. Default:
Europe/Sofia
BACKEND_PORT
No
Backend listen port. Default:
3001
FRONTEND_PORT
No
Frontend listen port. Default:
5175
DEV_BYPASS_AUTH
No
Set
true
to skip Authentik header check during local dev
Variable
DB_PASSWORD
NOTIFIER_URL
NOTIFIER_CHANNEL
NOTIFY_DEFAULT_PHONE
TZ
BACKEND_PORT
FRONTEND_PORT
DEV_BYPASS_AUTH
Required
Yes
No
No
No
No
No
No
No
Description
PostgreSQL password
Base URL of the notifier service
Channel to use (
viber
, etc.) Default:
viber
Phone number for payment notifications
Timezone for SMS date parsing. Default:
Europe/Sofia
Backend listen port. Default:
3001
Frontend listen port. Default:
5175
Set
true
to skip Authentik header check during local dev
Authentication
Authentication
In production, route the frontend through
Nginx Proxy Manager
with an
Authentik forward-auth
provider. NPM injects
X-authentik-username
(and optionally
X-authentik-email
,
X-authentik-groups
) into every proxied request.
The backend reads these headers and rejects requests without them (unless
DEV_BYPASS_AUTH=true
).
The public endpoints below are exempt and require no authentication:
GET /api/health
POST /api/payments/ingest
Sign-out link:
/outpost.goauthentik.io/sign_out
API Reference
API Reference
Health
Health
GET /api/health
GET /api/health
Public. Returns service status and database connection info.
{
"status"
:
"ok"
,
"timestamp"
:
"2026-05-09T10:00:00.000Z"
,
"storage"
:
{
"type"
:
"PostgreSQL"
,
"host"
:
"db"
,
"database"
:
"finance_hub"
}
}
Payments (Transaction Imports)
Payments (Transaction Imports)
Base path:
/api/payments
All endpoints require authentication except
/ingest
.
POST /api/payments/ingest
POST /api/payments/ingest
Public.
Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.
Rate limited to
200 requests/minute
.
Returns
409
if the exact message was already imported.
SMS body:
{
"message"
:
"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR."
}
Structured (Apple Wallet) body:
{
"ingestMode"
:
"apple_wallet"
,
"amount"
:
12.50
,
"recipient"
:
"Starbucks"
,
"type"
:
"WALLET"
,
"card"
:
"4444"
,
"date"
:
"2026-05-09T10:30:00Z"
,
"balance"
:
null
}
Response:
201 Created
— the created import record.
Duplicate:
409 Conflict
—
{ "error": "Already imported", "existing": { ... } }
GET /api/payments
GET /api/payments
List transaction imports with filtering, sorting, and pagination.
Query parameters:
Parameter
Type
Description
page
int
Page number. Default:
1
limit
int
Records per page, max 200. Default:
50
sortBy
string
Field to sort by:
date
,
amount
,
balance
,
recipient
,
type
,
source
,
createdAt
,
status
. Default:
createdAt
sortDir
string
asc
or
desc
. Default:
desc
status
string
Filter by status:
UNPROCESSED
,
SENT
,
SKIPPED
source
string
Filter by source:
INGEST
,
UPLOAD
type
string
Filter by transaction type (e.g.
POS
,
ATM
)
tag
string
Filter by tag name
recipient
string
Case-insensitive substring match on recipient
search
string
Case-insensitive search across
rawMessage
and
recipient
dateFrom
date
ISO date string — inclusive start of date range
dateTo
date
ISO date string — inclusive end of date range (interpreted as end of day)
hideBalanceAlerts
boolean
true
to exclude balance-notification SMS and records with no parsed amount
Parameter
page
limit
sortBy
sortDir
status
source
type
tag
recipient
search
dateFrom
dateTo
hideBalanceAlerts...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"EU alternatives to github : r/github","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"reddit.com","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DNS / Nameservers | Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DNS / Nameservers | Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Your old PC can run Windows 11 in a VM, but not on bare metal - kovaliklukas@gmail.com - Gmail","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Your old PC can run Windows 11 in a VM, but not on bare metal - kovaliklukas@gmail.com - Gmail","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"(25) Quora","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(25) Quora","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Select: transactions - db - Adminer","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Select: transactions - db - Adminer","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude Code | Claude Platform","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude Code | Claude Platform","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"EU alternatives to github : r/github","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EU alternatives to github : r/github","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Dashboard","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Issues","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull Requests","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull Requests","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Milestones","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Milestones","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Explore","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Explore","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Notifications","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"lakylak","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"lakylak","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"finance-hub","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"finance-hub","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Private","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RSS Feed","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unwatch","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"1","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Star","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"0","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Fork","depth":8,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fork","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Issues","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull Requests","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull Requests","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Projects","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Projects","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Releases","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Releases","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Activity","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Activity","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 Commits","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commits","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1 Branch","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Branch","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Tags","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tags","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"main","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Pull Request","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Go to file","depth":9,"on_screen":false,"help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"9a757fe67e","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9a757fe67e","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add README with project description, API reference, and usage guide","depth":11,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add README with project description, API reference, and usage guide","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"...","depth":10,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"41 minutes ago","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"backend","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"backend","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frontend","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frontend","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"scripts","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"scripts","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".env.example","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".env.example","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".gitignore","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".gitignore","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"docker-compose.yml","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"README.md","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"README.md","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add README with project description, API reference, and usage guide","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add README with project description, API reference, and usage guide","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"41 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"README.md Escape Edit File","depth":8,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"README.md","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"README.md","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Escape","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Edit File","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Finance Hub","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Finance Hub","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Replaces two separate tools:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"payments-logger","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— ingested DSK Bank SMS notifications","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dsk-uploader","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— parsed DSK Bank CSV exports and sent them to Notion","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Features","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Features","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SMS ingest","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deduplication","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— exact","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rawMessage","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"match prevents duplicate imports from re-uploads or re-runs of the reimport script","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto-tagging","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— new imports for a known recipient are automatically tagged based on tag history for that payee","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Rule-based tagging","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deduplication display","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— same-day same-amount records from SMS and CSV are shown as a single \"SMS + CSV\" row in the UI","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notification forwarding","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— sends payment summaries to a configurable notifier service (Viber, etc.)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tag management","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— create, colour-code, and remove tags on individual transactions","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Settings","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— delegated to Authentik via NPM reverse proxy header injection; no local user accounts","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Tech Stack","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tech Stack","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layer","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Technology","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Node.js 20, Express, Prisma 5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Database","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL 16","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV parsing","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"csv-parse","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iconv-lite","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(cp1251 + UTF-8 BOM)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"File upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"multer","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(memory storage)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"React 18, Vite, Tailwind CSS, Lucide React","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth via","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"header","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Container","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Docker Compose","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layer","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Database","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV parsing","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"File upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Container","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Technology","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Node.js 20, Express, Prisma 5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL 16","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"csv-parse","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iconv-lite","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(cp1251 + UTF-8 BOM)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"multer","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(memory storage)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"React 18, Vite, Tailwind CSS, Lucide React","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth via","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"header","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Docker Compose","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Quick Start","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Quick Start","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"1. Clone and configure","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Clone and configure","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"git clone git@gitea.com:lakylak/finance-hub.git","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cd","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"finance-hub\ncp .env.example .env","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"# Edit .env — set DB_PASSWORD and notifier settings","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"2. Start","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. Start","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"docker compose up -d --build","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Services:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Default port","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend API","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Adminer (DB UI)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8092","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend API","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Adminer (DB UI)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Default port","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8092","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"3. First run","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. First run","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Prisma migrations run automatically on backend startup via","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prisma migrate deploy","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Environment variables","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Environment variables","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Variable","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Required","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB_PASSWORD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Yes","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL password","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_URL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base URL of the notifier service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_CHANNEL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Channel to use (","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.) Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFY_DEFAULT_PHONE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Phone number for payment notifications","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TZ","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Timezone for SMS date parsing. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Europe/Sofia","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BACKEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FRONTEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Set","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to skip Authentik header check during local dev","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Variable","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB_PASSWORD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_URL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_CHANNEL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFY_DEFAULT_PHONE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TZ","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BACKEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FRONTEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Required","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Yes","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL password","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base URL of the notifier service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Channel to use (","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.) Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Phone number for payment notifications","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Timezone for SMS date parsing. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Europe/Sofia","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Set","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to skip Authentik header check during local dev","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Authentication","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentication","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"In production, route the frontend through","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"with an","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider. NPM injects","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and optionally","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-email","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-groups","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") into every proxied request.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The backend reads these headers and rejects requests without them (unless","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH=true","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":").","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The public endpoints below are exempt and require no authentication:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/health","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POST /api/payments/ingest","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Sign-out link:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/outpost.goauthentik.io/sign_out","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"API Reference","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"API Reference","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Health","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Health","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"GET /api/health","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/health","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Public. Returns service status and database connection info.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"status\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"ok\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"timestamp\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"2026-05-09T10:00:00.000Z\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"storage\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"type\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"PostgreSQL\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"host\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"db\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"database\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"finance_hub\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Payments (Transaction Imports)","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Payments (Transaction Imports)","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base path:","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/api/payments","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"All endpoints require authentication except","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ingest","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"POST /api/payments/ingest","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POST /api/payments/ingest","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Public.","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Rate limited to","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"200 requests/minute","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Returns","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"409","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if the exact message was already imported.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SMS body:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"message\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR.\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Structured (Apple Wallet) body:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"ingestMode\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"apple_wallet\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"amount\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.50","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"recipient\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"Starbucks\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"type\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"WALLET\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"card\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"4444\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"date\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"2026-05-09T10:30:00Z\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"balance\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Response:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"201 Created","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— the created import record.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Duplicate:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"409 Conflict","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{ \"error\": \"Already imported\", \"existing\": { ... } }","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"GET /api/payments","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/payments","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"List transaction imports with filtering, sorting, and pagination.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Query parameters:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Parameter","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"bounds":{"left":0.034027778,"top":0.0,"width":0.060763888,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"page","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"int","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Page number. Default:","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":13,"bounds":{"left":0.016666668,"top":0.0,"width":0.0055555557,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"int","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Records per page, max 200. Default:","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"50","depth":13,"bounds":{"left":0.08958333,"top":0.0,"width":0.011458334,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortBy","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Field to sort by:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":13,"bounds":{"left":0.0,"top":0.037777778,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.009722223,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":13,"bounds":{"left":0.019791666,"top":0.037777778,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.057638887,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"balance","depth":13,"bounds":{"left":0.067708336,"top":0.037777778,"width":0.039583333,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.11111111,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"bounds":{"left":0.12118056,"top":0.037777778,"width":0.051041666,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.17604166,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"bounds":{"left":0.18611111,"top":0.037777778,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.2125,"top":0.035,"width":0.003125,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"bounds":{"left":0.0,"top":0.064444445,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.0,"top":0.061666667,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createdAt","depth":13,"bounds":{"left":0.0,"top":0.064444445,"width":0.05138889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.004513889,"top":0.061666667,"width":0.005902778,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"bounds":{"left":0.014236111,"top":0.064444445,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". Default:","depth":12,"bounds":{"left":0.052083332,"top":0.061666667,"width":0.048611112,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createdAt","depth":13,"bounds":{"left":0.10451389,"top":0.064444445,"width":0.05138889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortDir","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"asc","depth":13,"bounds":{"left":0.0,"top":0.10555556,"width":0.017013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"or","depth":12,"bounds":{"left":0.0,"top":0.10277778,"width":0.016319444,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"desc","depth":13,"bounds":{"left":0.0,"top":0.10555556,"width":0.022916667,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". Default:","depth":12,"bounds":{"left":0.0,"top":0.10277778,"width":0.048611112,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"desc","depth":13,"bounds":{"left":0.021527778,"top":0.10555556,"width":0.022916667,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by status:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"UNPROCESSED","depth":13,"bounds":{"left":0.0,"top":0.14666666,"width":0.0625,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.048611112,"top":0.14388889,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SENT","depth":13,"bounds":{"left":0.058680557,"top":0.14666666,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.08506945,"top":0.14388889,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SKIPPED","depth":13,"bounds":{"left":0.09513889,"top":0.14666666,"width":0.039583333,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by source:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INGEST","depth":13,"bounds":{"left":0.0,"top":0.18777777,"width":0.034375,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.023263888,"top":0.185,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"UPLOAD","depth":13,"bounds":{"left":0.033333335,"top":0.18777777,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by transaction type (e.g.","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POS","depth":13,"bounds":{"left":0.05590278,"top":0.22888888,"width":0.017013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.07673611,"top":0.22611111,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ATM","depth":13,"bounds":{"left":0.08680555,"top":0.22888888,"width":0.017013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":12,"bounds":{"left":0.10763889,"top":0.22611111,"width":0.0038194444,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"tag","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by tag name","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Case-insensitive substring match on recipient","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"search","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Case-insensitive search across","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rawMessage","depth":13,"bounds":{"left":0.061458334,"top":0.35222223,"width":0.05659722,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":12,"bounds":{"left":0.121875,"top":0.34944445,"width":0.025,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"bounds":{"left":0.15069444,"top":0.35222223,"width":0.051041666,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateFrom","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ISO date string — inclusive start of date range","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateTo","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ISO date string — inclusive end of date range (interpreted as end of day)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"hideBalanceAlerts","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"boolean","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"bounds":{"left":0.0,"top":0.50222224,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to exclude balance-notification SMS and records with no parsed amount","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Parameter","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"page","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortBy","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortDir","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"tag","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"search","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateFrom","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateTo","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"hideBalanceAlerts","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8880139889786857011
|
4487437056102435094
|
click
|
accessibility
|
NULL
|
EU alternatives to github : r/github
reddit.com
Pu EU alternatives to github : r/github
reddit.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Select: transactions - db - Adminer
Select: transactions - db - Adminer
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Close tab
EU alternatives to github : r/github
EU alternatives to github : r/github
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Dashboard
Issues
Issues
Pull Requests
Pull Requests
Milestones
Milestones
Explore
Explore
Notifications
lakylak
lakylak
/
finance-hub
finance-hub
Private
RSS Feed
Unwatch
1
1
Star
0
0
Fork
Fork
0
0
Code
Code
Issues
Issues
Pull Requests
Pull Requests
Actions
Actions
Projects
Projects
Releases
Releases
Wiki
Wiki
Activity
Activity
Settings
Settings
2 Commits
2
Commits
1 Branch
1
Branch
0 Tags
0
Tags
main
New Pull Request
Go to file
Code
Code
Lukas Kovalik
9a757fe67e
9a757fe67e
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
...
41 minutes ago
backend
backend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
frontend
frontend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
scripts
scripts
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.env.example
.env.example
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.gitignore
.gitignore
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
docker-compose.yml
docker-compose.yml
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
README.md
README.md
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
41 minutes ago
README.md Escape Edit File
README.md
README.md
Escape
Edit File
Finance Hub
Finance Hub
Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.
Replaces two separate tools:
payments-logger
— ingested DSK Bank SMS notifications
dsk-uploader
— parsed DSK Bank CSV exports and sent them to Notion
Features
Features
SMS ingest
— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance
CSV upload
— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)
Deduplication
— exact
rawMessage
match prevents duplicate imports from re-uploads or re-runs of the reimport script
Auto-tagging
— new imports for a known recipient are automatically tagged based on tag history for that payee
Rule-based tagging
— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)
Deduplication display
— same-day same-amount records from SMS and CSV are shown as a single "SMS + CSV" row in the UI
Notification forwarding
— sends payment summaries to a configurable notifier service (Viber, etc.)
Tag management
— create, colour-code, and remove tags on individual transactions
Settings
— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS
Auth
— delegated to Authentik via NPM reverse proxy header injection; no local user accounts
Tech Stack
Tech Stack
Layer
Technology
Backend
Node.js 20, Express, Prisma 5
Database
PostgreSQL 16
CSV parsing
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
File upload
multer
(memory storage)
Frontend
React 18, Vite, Tailwind CSS, Lucide React
Auth
Authentik forward-auth via
X-authentik-username
header
Container
Docker Compose
Layer
Backend
Database
CSV parsing
File upload
Frontend
Auth
Container
Technology
Node.js 20, Express, Prisma 5
PostgreSQL 16
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
multer
(memory storage)
React 18, Vite, Tailwind CSS, Lucide React
Authentik forward-auth via
X-authentik-username
header
Docker Compose
Quick Start
Quick Start
1. Clone and configure
1. Clone and configure
git clone [EMAIL]:lakylak/finance-hub.git
cd
finance-hub
cp .env.example .env
# Edit .env — set DB_PASSWORD and notifier settings
2. Start
2. Start
docker compose up -d --build
Services:
Service
Default port
Frontend
5175
Backend API
3001
Adminer (DB UI)
8092
Service
Frontend
Backend API
Adminer (DB UI)
Default port
5175
3001
8092
3. First run
3. First run
Prisma migrations run automatically on backend startup via
prisma migrate deploy
.
Environment variables
Environment variables
Variable
Required
Description
DB_PASSWORD
Yes
PostgreSQL password
NOTIFIER_URL
No
Base URL of the notifier service
NOTIFIER_CHANNEL
No
Channel to use (
viber
, etc.) Default:
viber
NOTIFY_DEFAULT_PHONE
No
Phone number for payment notifications
TZ
No
Timezone for SMS date parsing. Default:
Europe/Sofia
BACKEND_PORT
No
Backend listen port. Default:
3001
FRONTEND_PORT
No
Frontend listen port. Default:
5175
DEV_BYPASS_AUTH
No
Set
true
to skip Authentik header check during local dev
Variable
DB_PASSWORD
NOTIFIER_URL
NOTIFIER_CHANNEL
NOTIFY_DEFAULT_PHONE
TZ
BACKEND_PORT
FRONTEND_PORT
DEV_BYPASS_AUTH
Required
Yes
No
No
No
No
No
No
No
Description
PostgreSQL password
Base URL of the notifier service
Channel to use (
viber
, etc.) Default:
viber
Phone number for payment notifications
Timezone for SMS date parsing. Default:
Europe/Sofia
Backend listen port. Default:
3001
Frontend listen port. Default:
5175
Set
true
to skip Authentik header check during local dev
Authentication
Authentication
In production, route the frontend through
Nginx Proxy Manager
with an
Authentik forward-auth
provider. NPM injects
X-authentik-username
(and optionally
X-authentik-email
,
X-authentik-groups
) into every proxied request.
The backend reads these headers and rejects requests without them (unless
DEV_BYPASS_AUTH=true
).
The public endpoints below are exempt and require no authentication:
GET /api/health
POST /api/payments/ingest
Sign-out link:
/outpost.goauthentik.io/sign_out
API Reference
API Reference
Health
Health
GET /api/health
GET /api/health
Public. Returns service status and database connection info.
{
"status"
:
"ok"
,
"timestamp"
:
"2026-05-09T10:00:00.000Z"
,
"storage"
:
{
"type"
:
"PostgreSQL"
,
"host"
:
"db"
,
"database"
:
"finance_hub"
}
}
Payments (Transaction Imports)
Payments (Transaction Imports)
Base path:
/api/payments
All endpoints require authentication except
/ingest
.
POST /api/payments/ingest
POST /api/payments/ingest
Public.
Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.
Rate limited to
200 requests/minute
.
Returns
409
if the exact message was already imported.
SMS body:
{
"message"
:
"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR."
}
Structured (Apple Wallet) body:
{
"ingestMode"
:
"apple_wallet"
,
"amount"
:
12.50
,
"recipient"
:
"Starbucks"
,
"type"
:
"WALLET"
,
"card"
:
"4444"
,
"date"
:
"2026-05-09T10:30:00Z"
,
"balance"
:
null
}
Response:
201 Created
— the created import record.
Duplicate:
409 Conflict
—
{ "error": "Already imported", "existing": { ... } }
GET /api/payments
GET /api/payments
List transaction imports with filtering, sorting, and pagination.
Query parameters:
Parameter
Type
Description
page
int
Page number. Default:
1
limit
int
Records per page, max 200. Default:
50
sortBy
string
Field to sort by:
date
,
amount
,
balance
,
recipient
,
type
,
source
,
createdAt
,
status
. Default:
createdAt
sortDir
string
asc
or
desc
. Default:
desc
status
string
Filter by status:
UNPROCESSED
,
SENT
,
SKIPPED
source
string
Filter by source:
INGEST
,
UPLOAD
type
string
Filter by transaction type (e.g.
POS
,
ATM
)
tag
string
Filter by tag name
recipient
string
Case-insensitive substring match on recipient
search
string
Case-insensitive search across
rawMessage
and
recipient
dateFrom
date
ISO date string — inclusive start of date range
dateTo
date
ISO date string — inclusive end of date range (interpreted as end of day)
hideBalanceAlerts
boolean
true
to exclude balance-notification SMS and records with no parsed amount
Parameter
page
limit
sortBy
sortDir
status
source
type
tag
recipient
search
dateFrom
dateTo
hideBalanceAlerts...
|
13821
|
NULL
|
NULL
|
NULL
|
|
26331
|
1093
|
5
|
2026-05-12T12:21:11.639696+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778588471639_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
EXPLORERV SCREENPIPE [SSH: NAS]• #reсycle› app> EXPLORERV SCREENPIPE [SSH: NAS]• #reсycle› app> logs> pipes• gitignore() app_settings.jsonE archive.dlE archive.db-bakE db.saliteE db.sqlite-shm= db.salite-wal$ screenpipe_fts_migrate.shS screenoioe sunc uodated.sh$ screenpipe_sync.sh= screennine.db() app_settings.jsonscreenpipe sync.shMs screenpipe its miarate.sh$ screenpipe snc updatedsh Upipe.md• .envscreenpipe_sync.sh# First-run migration:Ir old ris cables which used source 10 as rowid are derecced.they are dropped and rebuilt from the base tables. One-time slow stepsubseqvent runs are normal# Schema-drift tolerant:New columns added upstream get ALTER TABLE'd in. Inserts use explicit2026-04-15datetoday so far•/screenpipe_sync.sh —-reset-install-id # rotate install_id and exit•/screenpipe sync.sh =show-install-lo # print instalt la and exiu•/screenpipe_sync.sh —-list-installs#print all known installsset -euo pipefailCONFTGDB SRG="S SCREENPTPE DB:-SHOME/.screennine/db.salite?"NAS DB="SNAS MOUNT/archive.db"LOG_FILE="SHOME/ .screenpipe/sync. l0g"INSTALL_ID_FILE="SHOME/,screenpipe/.sync_install_id"speaker_embeddings audio_chunks audio_transcriptions audio_tags)ALL_SYNC_TABLES=("S(VISION_TABLES (e]}" "S(AUDIO_TABLES[e]}" "$(SHARED_TABLES (@l}")FTS_TABLES=trames tts elements tts ul events tts audio transcriptions ttsSCRIPT STARTES date +%s)local msg="[$(date '+8Y-8m-8d 8H:9M:%5')] $*"-a "SLOG_FILE"OUTPUTDEBUG CONSOLETERMINAIPORTS1[15:12:54) Getting Manifest…anthropic. claude-codeing extensieproductVersion: ( version: '1.119.0', date: '2026-05-05T11:23:50-07:00' ),pinned: talse,115:12:54.845operation: 3,isAppLicationScoped: false"/home/Admin/.vscode-server/extensions/extensions.ison'115.12-591 Extension simnature verification result for anthronic claude-code. Success Internal Code. 0. Execntede tme Duratione 12[15:13:00) Extracted extension to [URL_WITH_CREDENTIALS] (p)1lK lukas . ProScreenpipe data sync and retention management vchive.db /volume1/Test/screenpipe/archive.db.bak-pre-installid'If anything goes sideways you restore from this and re-run the oldscript.2. Pause the cron job to avoid a race with the migration:bashcrontad -e# comment out the screenpipe sync.sh line3. Pick a migration path.Path A - fast, recommended if archive is more than ~1 GB: runthe migration on the NAS over SSH.# from Macscp/Users/lukas/.screenpipe/screenpipe_fts_migrate.sh Adm1n€ssh Admine19-cd vollumetReply "npipechmod +x screenpipe fts migrate.sh•/screenpipe_fts_migrate.sh /volume1/Test/screenpipe/archive.You'll see a step-by-step log with timings. The populateframes_fts and populate audio_transcriptions_fts stepsare the slow onesPath B - simpler, fine for small archives: skip migration and letscreenpipe_sync.sh do it inline on its first run. Slower (SMB),but one command.4. Install the new sync script.cp screenpipe_sync.sh ~/.screenpipe/screenpipe_sync.shchmod +y ~/screennine/screennine svnc.sh5. Verify. Run a manual sync of yesterday to confirm everythingstill works:You're out of extra usage • Your limit resets at 7:20 PMBuy moreWrite a message.Opus 47 AdaptivevClaudo ic Aland can make mictakec Plosce doubкеtrо - Plauorm • In 1h 39.Screenpipe fts migrate • SH• lue 1z May 10.21-1copyspeaker 1d UNINDEXED, 1nstall 1d UNINDEXED, sOUrce 1d UNINDEXED175if table exists "frames": thenrun_with_timerINSERT INTO frames ftsful text, app name, window name. browser urinstall_id, source_id)SELECT fub text.aop name, window name, browser uni, instab id. idFROM frames WHERE full text IS NOT NULL AND fuil text e 11.if table exists "elements"; thenrun wich cimerINSERT INTO elements_fts(text, role, install_id, source_id, frame_id)SELECT text, role, install_id, id, frame_idGPOM elomonte WHEPE toyt TS NOT NIlI.iT caole exists "ul events, chelINSERT INTO U1 Ents ttstext content, app name, window title, element name, install 1d,source 1dSELECT text content, app name, window title, element name, install id.FROM u1 events WhERE text content is not NuLL,INSERT INTO audio_transcriptions_fts(transcription, device, speaker_id install id source 1d)SELECT transcription, COALESCE(device,"'), speaker_id, install_id, idFROM audio transcrintions WHERE transcrintion IS NOT NULL AND transcriotion le ft.207echo "Migration complete.""Final DB size: $(du -sh "$NAS_DB" | cut -f1)"echo "Sanity-check queries you can run:"echo "sqlite3 $NAS DB 'SELECT * FROM installs;'"echo"sqlite3 $NAS_DB |"SELECT sq1 FROM sqlite_master WHERE name='framesechosqlite3 SNAS_DB 'SELECT COUNT(*) FROM frames_fts; *™echo "salite3 $NAS DB 'SELECT COUNT(*) FROM audio transcriptions fts:"...
|
NULL
|
-8835814129153892671
|
NULL
|
click
|
ocr
|
NULL
|
EXPLORERV SCREENPIPE [SSH: NAS]• #reсycle› app> EXPLORERV SCREENPIPE [SSH: NAS]• #reсycle› app> logs> pipes• gitignore() app_settings.jsonE archive.dlE archive.db-bakE db.saliteE db.sqlite-shm= db.salite-wal$ screenpipe_fts_migrate.shS screenoioe sunc uodated.sh$ screenpipe_sync.sh= screennine.db() app_settings.jsonscreenpipe sync.shMs screenpipe its miarate.sh$ screenpipe snc updatedsh Upipe.md• .envscreenpipe_sync.sh# First-run migration:Ir old ris cables which used source 10 as rowid are derecced.they are dropped and rebuilt from the base tables. One-time slow stepsubseqvent runs are normal# Schema-drift tolerant:New columns added upstream get ALTER TABLE'd in. Inserts use explicit2026-04-15datetoday so far•/screenpipe_sync.sh —-reset-install-id # rotate install_id and exit•/screenpipe sync.sh =show-install-lo # print instalt la and exiu•/screenpipe_sync.sh —-list-installs#print all known installsset -euo pipefailCONFTGDB SRG="S SCREENPTPE DB:-SHOME/.screennine/db.salite?"NAS DB="SNAS MOUNT/archive.db"LOG_FILE="SHOME/ .screenpipe/sync. l0g"INSTALL_ID_FILE="SHOME/,screenpipe/.sync_install_id"speaker_embeddings audio_chunks audio_transcriptions audio_tags)ALL_SYNC_TABLES=("S(VISION_TABLES (e]}" "S(AUDIO_TABLES[e]}" "$(SHARED_TABLES (@l}")FTS_TABLES=trames tts elements tts ul events tts audio transcriptions ttsSCRIPT STARTES date +%s)local msg="[$(date '+8Y-8m-8d 8H:9M:%5')] $*"-a "SLOG_FILE"OUTPUTDEBUG CONSOLETERMINAIPORTS1[15:12:54) Getting Manifest…anthropic. claude-codeing extensieproductVersion: ( version: '1.119.0', date: '2026-05-05T11:23:50-07:00' ),pinned: talse,115:12:54.845operation: 3,isAppLicationScoped: false"/home/Admin/.vscode-server/extensions/extensions.ison'115.12-591 Extension simnature verification result for anthronic claude-code. Success Internal Code. 0. Execntede tme Duratione 12[15:13:00) Extracted extension to [URL_WITH_CREDENTIALS] (p)1lK lukas . ProScreenpipe data sync and retention management vchive.db /volume1/Test/screenpipe/archive.db.bak-pre-installid'If anything goes sideways you restore from this and re-run the oldscript.2. Pause the cron job to avoid a race with the migration:bashcrontad -e# comment out the screenpipe sync.sh line3. Pick a migration path.Path A - fast, recommended if archive is more than ~1 GB: runthe migration on the NAS over SSH.# from Macscp/Users/lukas/.screenpipe/screenpipe_fts_migrate.sh Adm1n€ssh Admine19-cd vollumetReply "npipechmod +x screenpipe fts migrate.sh•/screenpipe_fts_migrate.sh /volume1/Test/screenpipe/archive.You'll see a step-by-step log with timings. The populateframes_fts and populate audio_transcriptions_fts stepsare the slow onesPath B - simpler, fine for small archives: skip migration and letscreenpipe_sync.sh do it inline on its first run. Slower (SMB),but one command.4. Install the new sync script.cp screenpipe_sync.sh ~/.screenpipe/screenpipe_sync.shchmod +y ~/screennine/screennine svnc.sh5. Verify. Run a manual sync of yesterday to confirm everythingstill works:You're out of extra usage • Your limit resets at 7:20 PMBuy moreWrite a message.Opus 47 AdaptivevClaudo ic Aland can make mictakec Plosce doubкеtrо - Plauorm • In 1h 39.Screenpipe fts migrate • SH• lue 1z May 10.21-1copyspeaker 1d UNINDEXED, 1nstall 1d UNINDEXED, sOUrce 1d UNINDEXED175if table exists "frames": thenrun_with_timerINSERT INTO frames ftsful text, app name, window name. browser urinstall_id, source_id)SELECT fub text.aop name, window name, browser uni, instab id. idFROM frames WHERE full text IS NOT NULL AND fuil text e 11.if table exists "elements"; thenrun wich cimerINSERT INTO elements_fts(text, role, install_id, source_id, frame_id)SELECT text, role, install_id, id, frame_idGPOM elomonte WHEPE toyt TS NOT NIlI.iT caole exists "ul events, chelINSERT INTO U1 Ents ttstext content, app name, window title, element name, install 1d,source 1dSELECT text content, app name, window title, element name, install id.FROM u1 events WhERE text content is not NuLL,INSERT INTO audio_transcriptions_fts(transcription, device, speaker_id install id source 1d)SELECT transcription, COALESCE(device,"'), speaker_id, install_id, idFROM audio transcrintions WHERE transcrintion IS NOT NULL AND transcriotion le ft.207echo "Migration complete.""Final DB size: $(du -sh "$NAS_DB" | cut -f1)"echo "Sanity-check queries you can run:"echo "sqlite3 $NAS DB 'SELECT * FROM installs;'"echo"sqlite3 $NAS_DB |"SELECT sq1 FROM sqlite_master WHERE name='framesechosqlite3 SNAS_DB 'SELECT COUNT(*) FROM frames_fts; *™echo "salite3 $NAS DB 'SELECT COUNT(*) FROM audio transcriptions fts:"...
|
NULL
|
NULL
|
NULL
|
NULL
|